From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- image/AnimationFrameBuffer.cpp | 471 +++ image/AnimationFrameBuffer.h | 480 +++ image/AnimationParams.h | 55 + image/AnimationSurfaceProvider.cpp | 534 ++++ image/AnimationSurfaceProvider.h | 138 + image/AutoRestoreSVGState.h | 72 + image/BMPHeaders.h | 53 + image/BlobSurfaceProvider.cpp | 310 ++ image/BlobSurfaceProvider.h | 140 + image/ClippedImage.cpp | 512 +++ image/ClippedImage.h | 98 + image/CopyOnWrite.h | 247 ++ image/DecodePool.cpp | 203 ++ image/DecodePool.h | 108 + image/DecodedSurfaceProvider.cpp | 234 ++ image/DecodedSurfaceProvider.h | 93 + image/Decoder.cpp | 570 ++++ image/Decoder.h | 641 ++++ image/DecoderFactory.cpp | 446 +++ image/DecoderFactory.h | 210 ++ image/DecoderFlags.h | 56 + image/Downscaler.cpp | 302 ++ image/Downscaler.h | 146 + image/DownscalingFilter.h | 312 ++ image/DynamicImage.cpp | 298 ++ image/DynamicImage.h | 69 + image/FrameAnimator.cpp | 519 +++ image/FrameAnimator.h | 343 ++ image/FrameTimeout.h | 114 + image/FrozenImage.cpp | 148 + image/FrozenImage.h | 83 + image/ICOFileHeaders.h | 77 + image/IDecodingTask.cpp | 221 ++ image/IDecodingTask.h | 121 + image/IProgressObserver.h | 57 + image/ISurfaceProvider.h | 346 ++ image/Image.cpp | 261 ++ image/Image.h | 419 +++ image/ImageBlocker.cpp | 59 + image/ImageBlocker.h | 33 + image/ImageCacheKey.cpp | 170 + image/ImageCacheKey.h | 87 + image/ImageFactory.cpp | 277 ++ image/ImageFactory.h | 88 + image/ImageLogging.h | 185 ++ image/ImageMemoryReporter.cpp | 185 ++ image/ImageMemoryReporter.h | 95 + image/ImageMetadata.h | 115 + image/ImageOps.cpp | 248 ++ image/ImageOps.h | 167 + image/ImageRegion.h | 273 ++ image/ImageWrapper.cpp | 289 ++ image/ImageWrapper.h | 78 + image/ImgDrawResult.h | 123 + image/LookupResult.h | 137 + image/MultipartImage.cpp | 300 ++ image/MultipartImage.h | 81 + image/Orientation.h | 111 + image/OrientedImage.cpp | 349 ++ image/OrientedImage.h | 102 + image/PlaybackType.h | 40 + image/ProgressTracker.cpp | 561 ++++ image/ProgressTracker.h | 264 ++ image/RasterImage.cpp | 1785 +++++++++++ image/RasterImage.h | 484 +++ image/Resolution.h | 90 + image/SVGDocumentWrapper.cpp | 401 +++ image/SVGDocumentWrapper.h | 157 + image/SVGDrawingCallback.h | 39 + image/SVGDrawingParameters.h | 60 + image/ScriptedNotificationObserver.cpp | 73 + image/ScriptedNotificationObserver.h | 36 + image/ShutdownTracker.cpp | 68 + image/ShutdownTracker.h | 45 + image/SourceBuffer.cpp | 706 +++++ image/SourceBuffer.h | 499 +++ image/StreamingLexer.h | 751 +++++ image/SurfaceCache.cpp | 1970 ++++++++++++ image/SurfaceCache.h | 509 +++ image/SurfaceCacheUtils.cpp | 17 + image/SurfaceCacheUtils.h | 33 + image/SurfaceFilters.h | 1454 +++++++++ image/SurfaceFlags.h | 78 + image/SurfacePipe.cpp | 162 + image/SurfacePipe.h | 848 +++++ image/SurfacePipeFactory.h | 679 ++++ image/VectorImage.cpp | 1642 ++++++++++ image/VectorImage.h | 157 + image/WebRenderImageProvider.h | 65 + image/build/components.conf | 85 + image/build/moz.build | 26 + image/build/nsImageModule.cpp | 88 + image/build/nsImageModule.h | 20 + image/decoders/EXIF.cpp | 519 +++ image/decoders/EXIF.h | 95 + image/decoders/GIF2.h | 67 + image/decoders/iccjpeg.c | 184 ++ image/decoders/iccjpeg.h | 65 + image/decoders/icon/android/moz.build | 13 + image/decoders/icon/android/nsIconChannel.cpp | 125 + image/decoders/icon/android/nsIconChannel.h | 45 + image/decoders/icon/components.conf | 29 + image/decoders/icon/gtk/moz.build | 20 + image/decoders/icon/gtk/nsIconChannel.cpp | 483 +++ image/decoders/icon/gtk/nsIconChannel.h | 53 + image/decoders/icon/mac/moz.build | 13 + image/decoders/icon/mac/nsIconChannel.h | 61 + image/decoders/icon/mac/nsIconChannelCocoa.mm | 505 +++ image/decoders/icon/moz.build | 39 + image/decoders/icon/nsIconProtocolHandler.cpp | 68 + image/decoders/icon/nsIconProtocolHandler.h | 25 + image/decoders/icon/nsIconURI.cpp | 654 ++++ image/decoders/icon/nsIconURI.h | 118 + image/decoders/icon/win/moz.build | 21 + image/decoders/icon/win/nsIconChannel.cpp | 1006 ++++++ image/decoders/icon/win/nsIconChannel.h | 65 + image/decoders/moz.build | 62 + image/decoders/nsAVIFDecoder.cpp | 1991 ++++++++++++ image/decoders/nsAVIFDecoder.h | 289 ++ image/decoders/nsBMPDecoder.cpp | 1275 ++++++++ image/decoders/nsBMPDecoder.h | 285 ++ image/decoders/nsGIFDecoder2.cpp | 1073 +++++++ image/decoders/nsGIFDecoder2.h | 166 + image/decoders/nsICODecoder.cpp | 709 +++++ image/decoders/nsICODecoder.h | 106 + image/decoders/nsIconDecoder.cpp | 125 + image/decoders/nsIconDecoder.h | 64 + image/decoders/nsJPEGDecoder.cpp | 999 ++++++ image/decoders/nsJPEGDecoder.h | 113 + image/decoders/nsJXLDecoder.cpp | 163 + image/decoders/nsJXLDecoder.h | 55 + image/decoders/nsPNGDecoder.cpp | 1035 ++++++ image/decoders/nsPNGDecoder.h | 148 + image/decoders/nsWebPDecoder.cpp | 605 ++++ image/decoders/nsWebPDecoder.h | 105 + image/encoders/bmp/moz.build | 15 + image/encoders/bmp/nsBMPEncoder.cpp | 716 +++++ image/encoders/bmp/nsBMPEncoder.h | 150 + image/encoders/ico/moz.build | 20 + image/encoders/ico/nsICOEncoder.cpp | 499 +++ image/encoders/ico/nsICOEncoder.h | 95 + image/encoders/jpeg/moz.build | 11 + image/encoders/jpeg/nsJPEGEncoder.cpp | 519 +++ image/encoders/jpeg/nsJPEGEncoder.h | 74 + image/encoders/moz.build | 13 + image/encoders/png/moz.build | 17 + image/encoders/png/nsPNGEncoder.cpp | 820 +++++ image/encoders/png/nsPNGEncoder.h | 79 + image/encoders/webp/moz.build | 15 + image/encoders/webp/nsWebPEncoder.cpp | 342 ++ image/encoders/webp/nsWebPEncoder.h | 63 + image/imgFrame.cpp | 729 +++++ image/imgFrame.h | 422 +++ image/imgICache.idl | 106 + image/imgIContainer.idl | 692 ++++ image/imgIContainerDebug.idl | 25 + image/imgIEncoder.idl | 146 + image/imgILoader.idl | 97 + image/imgINotificationObserver.idl | 59 + image/imgIRequest.idl | 276 ++ image/imgIScriptedNotificationObserver.idl | 22 + image/imgITools.idl | 207 ++ image/imgLoader.cpp | 3334 ++++++++++++++++++++ image/imgLoader.h | 534 ++++ image/imgRequest.cpp | 1231 ++++++++ image/imgRequest.h | 296 ++ image/imgRequestProxy.cpp | 1327 ++++++++ image/imgRequestProxy.h | 263 ++ image/imgTools.cpp | 649 ++++ image/imgTools.h | 36 + image/moz.build | 139 + image/nsIIconURI.idl | 92 + image/test/browser/animated.gif | Bin 0 -> 71479 bytes image/test/browser/animated2.gif | Bin 0 -> 66647 bytes image/test/browser/big.png | Bin 0 -> 129497 bytes image/test/browser/browser.ini | 20 + image/test/browser/browser_bug666317.js | 138 + image/test/browser/browser_docshell_type_editor.js | 134 + .../browser_docshell_type_editor/chrome.manifest | 1 + .../img/privileged.png | Bin 0 -> 90 bytes image/test/browser/browser_image.js | 261 ++ image/test/browser/browser_mozicon_file.js | 12 + .../browser_mozicon_file_sandbox_headless.js | 13 + ...ser_offscreen_image_in_out_of_process_iframe.js | 164 + image/test/browser/browser_sandbox_headless.ini | 8 + image/test/browser/empty.html | 2 + image/test/browser/head.js | 136 + image/test/browser/image.html | 23 + image/test/browser/imageX2.html | 14 + image/test/crashtests/1205923-1.html | 36 + image/test/crashtests/1210745-1.gif | Bin 0 -> 23 bytes image/test/crashtests/1212954-1.svg | 16 + image/test/crashtests/1235605.gif | Bin 0 -> 2360 bytes image/test/crashtests/1241728-1.html | 17 + image/test/crashtests/1241729-1.bmp | Bin 0 -> 548 bytes image/test/crashtests/1241729-1.html | 5 + image/test/crashtests/1242093-1.html | 22 + image/test/crashtests/1242778-1.png | Bin 0 -> 15929 bytes image/test/crashtests/1249576-1.png | Bin 0 -> 1169 bytes image/test/crashtests/1251091-1.html | 51 + image/test/crashtests/1251091-1.png | Bin 0 -> 95370 bytes image/test/crashtests/1253362-1.html | 11 + image/test/crashtests/1262549-1.gif | Bin 0 -> 428 bytes image/test/crashtests/1277397-1.jpg | Bin 0 -> 266 bytes image/test/crashtests/1277397-2.jpg | Bin 0 -> 1407 bytes image/test/crashtests/1355898-1.html | 45 + image/test/crashtests/1375842-1.html | 16 + image/test/crashtests/1413762-1.gif | Bin 0 -> 7866 bytes image/test/crashtests/1443232-1.gif | Bin 0 -> 42434 bytes image/test/crashtests/1443232-1.html | 30 + image/test/crashtests/1509998.gif | Bin 0 -> 92 bytes image/test/crashtests/1526717-1.html | 1 + image/test/crashtests/1526717-1.png | Bin 0 -> 318 bytes image/test/crashtests/1629490-1-iframe.html | 1 + image/test/crashtests/1629490-1.html | 49 + image/test/crashtests/1634839-1-iframe.html | 1 + image/test/crashtests/1634839-1.html | 51 + image/test/crashtests/1634839-2-iframe.html | 1 + image/test/crashtests/1634839-2.html | 51 + image/test/crashtests/1676172-1-iframe.html | 13 + image/test/crashtests/1676172-1.gif | Bin 0 -> 2439 bytes image/test/crashtests/1676172-1.html | 57 + image/test/crashtests/1763581-1-iframe.html | 15 + image/test/crashtests/1763581-1.gif | Bin 0 -> 70793 bytes image/test/crashtests/1763581-1.html | 28 + image/test/crashtests/1763581-1.sjs | 71 + image/test/crashtests/1765871-1.jpg | Bin 0 -> 6872 bytes image/test/crashtests/1814553.avif | Bin 0 -> 803330 bytes image/test/crashtests/1814561.avif | Bin 0 -> 2104 bytes image/test/crashtests/1814677.avif | Bin 0 -> 1607 bytes image/test/crashtests/1814708.avif | Bin 0 -> 1607 bytes image/test/crashtests/1814741.avif | Bin 0 -> 1607 bytes image/test/crashtests/1814774.avif | Bin 0 -> 2104 bytes image/test/crashtests/1817108.avif | Bin 0 -> 2105 bytes image/test/crashtests/256-height.ico | Bin 0 -> 154 bytes image/test/crashtests/256-width.ico | Bin 0 -> 154 bytes image/test/crashtests/463696.bmp | Bin 0 -> 1272 bytes image/test/crashtests/523528-1.gif | Bin 0 -> 132 bytes image/test/crashtests/523528-2.gif | Bin 0 -> 132 bytes image/test/crashtests/570451.png | Bin 0 -> 114 bytes image/test/crashtests/694165-1.xhtml | 510 +++ image/test/crashtests/732319-1.html | 2 + image/test/crashtests/83804-1.gif | Bin 0 -> 37 bytes image/test/crashtests/844403-1.html | 10 + image/test/crashtests/856616.gif | Bin 0 -> 27 bytes image/test/crashtests/89341-1.gif | Bin 0 -> 769 bytes image/test/crashtests/944353.jpg | Bin 0 -> 610965 bytes image/test/crashtests/colormap-range.gif | Bin 0 -> 5657 bytes image/test/crashtests/crashtests.list | 84 + image/test/crashtests/delayedframe.sjs | 44 + image/test/crashtests/delaytest.html | 59 + image/test/crashtests/discardframe.htm | 1 + image/test/crashtests/finite-apng.png | Bin 0 -> 5834 bytes image/test/crashtests/ie.png | Bin 0 -> 466589 bytes .../test/crashtests/invalid-disposal-method-1.gif | Bin 0 -> 167 bytes .../test/crashtests/invalid-disposal-method-2.gif | Bin 0 -> 167 bytes .../test/crashtests/invalid-disposal-method-3.gif | Bin 0 -> 167 bytes image/test/crashtests/invalid-icc-profile.jpg | Bin 0 -> 2568 bytes .../test/crashtests/invalid-size-second-frame.gif | Bin 0 -> 673 bytes image/test/crashtests/invalid-size.gif | Bin 0 -> 329 bytes image/test/crashtests/invalid_ico_height.ico | Bin 0 -> 894 bytes image/test/crashtests/invalid_ico_width.ico | Bin 0 -> 894 bytes image/test/crashtests/multiple-png-hassize.ico | Bin 0 -> 18096 bytes image/test/crashtests/out2.gif | Bin 0 -> 24129 bytes image/test/crashtests/ownerdiscard.html | 48 + image/test/crashtests/rainbow.gif | Bin 0 -> 6171 bytes image/test/crashtests/threeframes-end.gif | Bin 0 -> 16 bytes image/test/crashtests/threeframes-start.gif | Bin 0 -> 92 bytes image/test/crashtests/truncated-second-frame.png | Bin 0 -> 72247 bytes image/test/crashtests/unsized-svg.svg | 1 + image/test/fuzzing/TestDecoders.cpp | 180 ++ image/test/fuzzing/moz.build | 24 + image/test/gtest/Common.cpp | 1076 +++++++ image/test/gtest/Common.h | 600 ++++ image/test/gtest/TestADAM7InterpolatingFilter.cpp | 595 ++++ image/test/gtest/TestAnimationFrameBuffer.cpp | 895 ++++++ image/test/gtest/TestBlendAnimationFilter.cpp | 450 +++ image/test/gtest/TestCopyOnWrite.cpp | 237 ++ image/test/gtest/TestDecodeToSurface.cpp | 173 + image/test/gtest/TestDecoders.cpp | 1142 +++++++ image/test/gtest/TestDecodersPerf.cpp | 159 + image/test/gtest/TestDeinterlacingFilter.cpp | 636 ++++ image/test/gtest/TestDownscalingFilter.cpp | 231 ++ image/test/gtest/TestFrameAnimator.cpp | 130 + image/test/gtest/TestLoader.cpp | 118 + image/test/gtest/TestMetadata.cpp | 260 ++ image/test/gtest/TestRemoveFrameRectFilter.cpp | 311 ++ image/test/gtest/TestSourceBuffer.cpp | 822 +++++ image/test/gtest/TestStreamingLexer.cpp | 935 ++++++ image/test/gtest/TestSurfaceCache.cpp | 159 + image/test/gtest/TestSurfacePipeIntegration.cpp | 349 ++ image/test/gtest/TestSurfaceSink.cpp | 982 ++++++ image/test/gtest/TestSwizzleFilter.cpp | 120 + .../gtest/animated-with-extra-image-sub-blocks.gif | Bin 0 -> 434 bytes image/test/gtest/blend.avif | Bin 0 -> 1989 bytes image/test/gtest/blend.gif | Bin 0 -> 412 bytes image/test/gtest/blend.png | Bin 0 -> 339 bytes image/test/gtest/blend.webp | Bin 0 -> 160 bytes image/test/gtest/bug-1655846.avif | Bin 0 -> 256 bytes image/test/gtest/corrupt-with-bad-bmp-height.ico | Bin 0 -> 41663 bytes image/test/gtest/corrupt-with-bad-bmp-width.ico | Bin 0 -> 41663 bytes image/test/gtest/corrupt-with-bad-ico-bpp.ico | Bin 0 -> 3270 bytes image/test/gtest/corrupt.jpg | Bin 0 -> 2477 bytes image/test/gtest/downscaled.avif | Bin 0 -> 862 bytes image/test/gtest/downscaled.bmp | Bin 0 -> 30138 bytes image/test/gtest/downscaled.gif | Bin 0 -> 223 bytes image/test/gtest/downscaled.ico | Bin 0 -> 41662 bytes image/test/gtest/downscaled.icon | Bin 0 -> 40005 bytes image/test/gtest/downscaled.jpg | Bin 0 -> 6035 bytes image/test/gtest/downscaled.jxl | Bin 0 -> 599 bytes image/test/gtest/downscaled.png | Bin 0 -> 1015 bytes image/test/gtest/downscaled.webp | Bin 0 -> 56 bytes image/test/gtest/exif_resolution.jpg | Bin 0 -> 2018 bytes image/test/gtest/first-frame-green.avif | Bin 0 -> 1001 bytes image/test/gtest/first-frame-green.gif | Bin 0 -> 317 bytes image/test/gtest/first-frame-green.png | Bin 0 -> 364 bytes image/test/gtest/first-frame-green.webp | Bin 0 -> 154 bytes image/test/gtest/first-frame-padding.gif | Bin 0 -> 49 bytes .../gtest/gray-235-10bit-full-range-bt2020.avif | Bin 0 -> 310 bytes .../gtest/gray-235-10bit-full-range-bt601.avif | Bin 0 -> 310 bytes .../gtest/gray-235-10bit-full-range-bt709.avif | Bin 0 -> 310 bytes .../gtest/gray-235-10bit-full-range-grayscale.avif | Bin 0 -> 301 bytes .../gtest/gray-235-10bit-limited-range-bt2020.avif | Bin 0 -> 310 bytes .../gtest/gray-235-10bit-limited-range-bt601.avif | Bin 0 -> 310 bytes .../gtest/gray-235-10bit-limited-range-bt709.avif | Bin 0 -> 310 bytes .../gray-235-10bit-limited-range-grayscale.avif | Bin 0 -> 301 bytes .../gtest/gray-235-12bit-full-range-bt2020.avif | Bin 0 -> 307 bytes .../gtest/gray-235-12bit-full-range-bt601.avif | Bin 0 -> 307 bytes .../gtest/gray-235-12bit-full-range-bt709.avif | Bin 0 -> 307 bytes .../gtest/gray-235-12bit-full-range-grayscale.avif | Bin 0 -> 301 bytes .../gtest/gray-235-12bit-limited-range-bt2020.avif | Bin 0 -> 307 bytes .../gtest/gray-235-12bit-limited-range-bt601.avif | Bin 0 -> 307 bytes .../gtest/gray-235-12bit-limited-range-bt709.avif | Bin 0 -> 307 bytes .../gray-235-12bit-limited-range-grayscale.avif | Bin 0 -> 301 bytes .../gtest/gray-235-8bit-full-range-bt2020.avif | Bin 0 -> 309 bytes .../test/gtest/gray-235-8bit-full-range-bt601.avif | Bin 0 -> 309 bytes .../test/gtest/gray-235-8bit-full-range-bt709.avif | Bin 0 -> 309 bytes .../gtest/gray-235-8bit-full-range-grayscale.avif | Bin 0 -> 300 bytes .../gtest/gray-235-8bit-limited-range-bt2020.avif | Bin 0 -> 309 bytes .../gtest/gray-235-8bit-limited-range-bt601.avif | Bin 0 -> 309 bytes .../gtest/gray-235-8bit-limited-range-bt709.avif | Bin 0 -> 309 bytes .../gray-235-8bit-limited-range-grayscale.avif | Bin 0 -> 300 bytes image/test/gtest/green-1x1-truncated.gif | Bin 0 -> 53 bytes image/test/gtest/green-large-bmp.ico | Bin 0 -> 270398 bytes image/test/gtest/green-large-png.ico | Bin 0 -> 341 bytes image/test/gtest/green-multiple-sizes.ico | Bin 0 -> 14144 bytes image/test/gtest/green.avif | Bin 0 -> 321 bytes image/test/gtest/green.bmp | Bin 0 -> 30138 bytes image/test/gtest/green.gif | Bin 0 -> 156 bytes image/test/gtest/green.icc_srgb.webp | Bin 0 -> 3092 bytes image/test/gtest/green.ico | Bin 0 -> 41662 bytes image/test/gtest/green.icon | Bin 0 -> 40004 bytes image/test/gtest/green.jpg | Bin 0 -> 361 bytes image/test/gtest/green.jxl | Bin 0 -> 33 bytes image/test/gtest/green.png | Bin 0 -> 255 bytes image/test/gtest/green.webp | Bin 0 -> 42 bytes .../gtest/hdlr-nonzero-reserved-bug-1727033.avif | Bin 0 -> 294 bytes image/test/gtest/invalid-truncated-metadata.bmp | Bin 0 -> 54 bytes image/test/gtest/large.avif | Bin 0 -> 81500 bytes image/test/gtest/large.jxl | Bin 0 -> 315715 bytes image/test/gtest/large.webp | Bin 0 -> 168686 bytes image/test/gtest/moz.build | 156 + image/test/gtest/multilayer.avif | Bin 0 -> 64613 bytes image/test/gtest/no-frame-delay.gif | Bin 0 -> 317 bytes image/test/gtest/perf_cmyk.jpg | Bin 0 -> 18960 bytes image/test/gtest/perf_gray.jpg | Bin 0 -> 4479 bytes image/test/gtest/perf_gray.png | Bin 0 -> 4472 bytes image/test/gtest/perf_gray_alpha.png | Bin 0 -> 6847 bytes image/test/gtest/perf_srgb.gif | Bin 0 -> 1743 bytes image/test/gtest/perf_srgb.png | Bin 0 -> 2993 bytes image/test/gtest/perf_srgb_alpha.png | Bin 0 -> 3203 bytes image/test/gtest/perf_srgb_alpha_lossless.webp | Bin 0 -> 3134 bytes image/test/gtest/perf_srgb_alpha_lossy.webp | Bin 0 -> 5014 bytes image/test/gtest/perf_srgb_lossless.webp | Bin 0 -> 3134 bytes image/test/gtest/perf_srgb_lossy.webp | Bin 0 -> 4948 bytes image/test/gtest/perf_ycbcr.jpg | Bin 0 -> 15049 bytes image/test/gtest/rle4.bmp | Bin 0 -> 3686 bytes image/test/gtest/rle8.bmp | Bin 0 -> 1288 bytes image/test/gtest/stackcheck.avif | Bin 0 -> 194817 bytes .../transparent-green-50pct-10bit-yuv420.avif | Bin 0 -> 556 bytes .../transparent-green-50pct-10bit-yuv422.avif | Bin 0 -> 551 bytes .../transparent-green-50pct-10bit-yuv444.avif | Bin 0 -> 556 bytes .../transparent-green-50pct-12bit-yuv420.avif | Bin 0 -> 556 bytes .../transparent-green-50pct-12bit-yuv422.avif | Bin 0 -> 557 bytes .../transparent-green-50pct-12bit-yuv444.avif | Bin 0 -> 558 bytes .../gtest/transparent-green-50pct-8bit-yuv420.avif | Bin 0 -> 539 bytes .../gtest/transparent-green-50pct-8bit-yuv422.avif | Bin 0 -> 533 bytes .../gtest/transparent-green-50pct-8bit-yuv444.avif | Bin 0 -> 537 bytes image/test/gtest/transparent-ico-with-and-mask.ico | Bin 0 -> 3262 bytes image/test/gtest/transparent-if-within-ico.bmp | Bin 0 -> 4234 bytes image/test/gtest/transparent-no-alpha-header.webp | Bin 0 -> 120 bytes image/test/gtest/transparent.avif | Bin 0 -> 209480 bytes image/test/gtest/transparent.gif | Bin 0 -> 355 bytes image/test/gtest/transparent.jxl | Bin 0 -> 63244 bytes image/test/gtest/transparent.png | Bin 0 -> 419 bytes image/test/gtest/transparent.webp | Bin 0 -> 120 bytes .../test/gtest/valid-avif-colr-nclx-and-prof.avif | Bin 0 -> 883 bytes image/test/mochitest/12M-pixels-1.png | Bin 0 -> 22467 bytes image/test/mochitest/12M-pixels-2.png | Bin 0 -> 22467 bytes image/test/mochitest/6M-pixels.png | Bin 0 -> 10147 bytes image/test/mochitest/INT32_MIN.bmp | Bin 0 -> 60 bytes image/test/mochitest/animated-avif.avif | Bin 0 -> 1333 bytes image/test/mochitest/animated-gif-finalframe.gif | Bin 0 -> 72 bytes image/test/mochitest/animated-gif.gif | Bin 0 -> 146 bytes image/test/mochitest/animated-gif2.gif | Bin 0 -> 165 bytes .../mochitest/animated-gif_trailing-garbage.gif | Bin 0 -> 4030 bytes image/test/mochitest/animated1.gif | Bin 0 -> 4558 bytes image/test/mochitest/animated1.svg | 12 + image/test/mochitest/animated2.gif | Bin 0 -> 4558 bytes image/test/mochitest/animatedMask.gif | Bin 0 -> 4568 bytes image/test/mochitest/animation.svg | 5 + image/test/mochitest/animationPolling.js | 469 +++ image/test/mochitest/bad.jpg | Bin 0 -> 2477 bytes image/test/mochitest/big.png | Bin 0 -> 129497 bytes image/test/mochitest/blue.gif | Bin 0 -> 45 bytes image/test/mochitest/blue.png | Bin 0 -> 2745 bytes image/test/mochitest/bug1132427.gif | Bin 0 -> 634 bytes image/test/mochitest/bug1132427.html | 6 + image/test/mochitest/bug1180105-waiter.sjs | 29 + image/test/mochitest/bug1180105.sjs | 68 + image/test/mochitest/bug1217571-iframe.html | 17 + image/test/mochitest/bug1217571.jpg | Bin 0 -> 2679 bytes image/test/mochitest/bug1319025-ref.png | Bin 0 -> 347 bytes image/test/mochitest/bug1319025.png | Bin 0 -> 422 bytes image/test/mochitest/bug399925.gif | Bin 0 -> 1645 bytes image/test/mochitest/bug415761.ico | Bin 0 -> 766 bytes image/test/mochitest/bug468160.sjs | 5 + image/test/mochitest/bug478398_ONLY.png | Bin 0 -> 14139 bytes image/test/mochitest/bug490949-iframe.html | 7 + image/test/mochitest/bug490949.sjs | 32 + image/test/mochitest/bug496292-1.sjs | 31 + image/test/mochitest/bug496292-2.sjs | 31 + image/test/mochitest/bug496292-iframe-1.html | 7 + image/test/mochitest/bug496292-iframe-2.html | 7 + image/test/mochitest/bug496292-iframe-ref.html | 7 + image/test/mochitest/bug497665-iframe.html | 8 + image/test/mochitest/bug497665.sjs | 33 + image/test/mochitest/bug552605.sjs | 12 + image/test/mochitest/bug657191.sjs | 26 + image/test/mochitest/bug671906-iframe.html | 7 + image/test/mochitest/bug671906.sjs | 34 + image/test/mochitest/bug733553-informant.sjs | 13 + image/test/mochitest/bug733553.sjs | 104 + image/test/mochitest/bug767779.sjs | 56 + image/test/mochitest/bug89419-iframe.html | 7 + image/test/mochitest/bug89419.sjs | 12 + image/test/mochitest/bug900200-ref.png | Bin 0 -> 660 bytes image/test/mochitest/bug900200.png | Bin 0 -> 840 bytes image/test/mochitest/child.html | 22 + image/test/mochitest/chrome.ini | 7 + image/test/mochitest/clear.avif | Bin 0 -> 1975 bytes image/test/mochitest/clear.gif | Bin 0 -> 321 bytes image/test/mochitest/clear.png | Bin 0 -> 622 bytes image/test/mochitest/clear.webp | Bin 0 -> 202 bytes image/test/mochitest/clear2-results.gif | Bin 0 -> 177 bytes image/test/mochitest/clear2.gif | Bin 0 -> 219 bytes image/test/mochitest/clear2.webp | Bin 0 -> 228 bytes image/test/mochitest/damon.jpg | Bin 0 -> 2679 bytes image/test/mochitest/error-early.png | 1 + image/test/mochitest/filter-final.svg | 9 + image/test/mochitest/filter.svg | 9 + image/test/mochitest/finite-apng.png | Bin 0 -> 5834 bytes image/test/mochitest/first-frame-padding.gif | Bin 0 -> 49 bytes image/test/mochitest/green-background.html | 30 + image/test/mochitest/green.png | Bin 0 -> 255 bytes image/test/mochitest/grey.png | Bin 0 -> 256 bytes image/test/mochitest/ico-bmp-opaque.ico | Bin 0 -> 1094 bytes image/test/mochitest/ico-bmp-transparent.ico | Bin 0 -> 4286 bytes image/test/mochitest/iframe.html | 5 + image/test/mochitest/imgutils.js | 137 + image/test/mochitest/infinite-apng.png | Bin 0 -> 1169 bytes image/test/mochitest/infinite.avif | Bin 0 -> 2311 bytes image/test/mochitest/infinite.webp | Bin 0 -> 3742 bytes image/test/mochitest/invalid.jpg | 1 + image/test/mochitest/keep.gif | Bin 0 -> 321 bytes image/test/mochitest/keep.png | Bin 0 -> 622 bytes image/test/mochitest/keep.webp | Bin 0 -> 152 bytes image/test/mochitest/lime-anim-100x100-2.svg | 6 + image/test/mochitest/lime-anim-100x100.svg | 7 + image/test/mochitest/lime-css-anim-100x100.svg | 19 + image/test/mochitest/lime100x100.svg | 4 + image/test/mochitest/mochitest.ini | 184 ++ image/test/mochitest/mq_dynamic_svg_ref.html | 38 + image/test/mochitest/mq_dynamic_svg_test.html | 61 + image/test/mochitest/opaque.bmp | Bin 0 -> 1086 bytes image/test/mochitest/over.png | Bin 0 -> 525 bytes image/test/mochitest/purple.gif | Bin 0 -> 86 bytes image/test/mochitest/rainbow.gif | Bin 0 -> 1572 bytes image/test/mochitest/red.gif | Bin 0 -> 43 bytes image/test/mochitest/red.png | Bin 0 -> 82 bytes image/test/mochitest/ref-iframe.html | 6 + image/test/mochitest/restore-previous.gif | Bin 0 -> 457 bytes image/test/mochitest/restore-previous.png | Bin 0 -> 622 bytes image/test/mochitest/rillybad.jpg | Bin 0 -> 11142 bytes image/test/mochitest/schrep.png | Bin 0 -> 38767 bytes image/test/mochitest/shaver.png | Bin 0 -> 52045 bytes image/test/mochitest/short_header.gif | Bin 0 -> 1488 bytes image/test/mochitest/source.png | Bin 0 -> 525 bytes image/test/mochitest/test_animSVGImage.html | 124 + image/test/mochitest/test_animSVGImage2.html | 124 + image/test/mochitest/test_animated_css_image.html | 223 ++ image/test/mochitest/test_animated_gif.html | 50 + image/test/mochitest/test_animation.html | 45 + image/test/mochitest/test_animation2.html | 49 + image/test/mochitest/test_animation_operators.html | 168 + .../test/mochitest/test_background_image_anim.html | 44 + image/test/mochitest/test_bug1132427.html | 94 + image/test/mochitest/test_bug1180105.html | 46 + image/test/mochitest/test_bug1217571.html | 43 + image/test/mochitest/test_bug1325080.html | 37 + image/test/mochitest/test_bug399925.html | 102 + image/test/mochitest/test_bug415761.html | 117 + image/test/mochitest/test_bug435296.html | 85 + image/test/mochitest/test_bug466586.html | 58 + image/test/mochitest/test_bug468160.html | 29 + image/test/mochitest/test_bug478398.html | 87 + image/test/mochitest/test_bug490949.html | 112 + image/test/mochitest/test_bug496292.html | 130 + image/test/mochitest/test_bug497665.html | 88 + image/test/mochitest/test_bug552605-1.html | 56 + image/test/mochitest/test_bug552605-2.html | 53 + image/test/mochitest/test_bug553982.html | 39 + image/test/mochitest/test_bug601470.html | 45 + image/test/mochitest/test_bug614392.html | 42 + image/test/mochitest/test_bug657191.html | 34 + image/test/mochitest/test_bug671906.html | 71 + image/test/mochitest/test_bug733553.html | 92 + image/test/mochitest/test_bug767779.html | 44 + image/test/mochitest/test_bug865919.html | 53 + image/test/mochitest/test_bug89419-1.html | 68 + image/test/mochitest/test_bug89419-2.html | 70 + image/test/mochitest/test_bullet_animation.html | 59 + .../mochitest/test_canvas_frame_animation.html | 24 + image/test/mochitest/test_changeOfSource.html | 62 + image/test/mochitest/test_changeOfSource2.html | 47 + .../test/mochitest/test_discardAnimatedImage.html | 218 ++ .../test_discardFinishedAnimatedImage.html | 144 + .../mochitest/test_discardFramesAnimatedImage.html | 268 ++ image/test/mochitest/test_drawDiscardedImage.html | 85 + image/test/mochitest/test_error_events.html | 67 + image/test/mochitest/test_has_transparency.html | 169 + .../mochitest/test_image_cache_notification.html | 47 + .../mochitest/test_image_crossorigin_data_url.html | 28 + image/test/mochitest/test_mq_dynamic_svg.html | 49 + image/test/mochitest/test_net_failedtoprocess.html | 52 + image/test/mochitest/test_removal_ondecode.html | 160 + image/test/mochitest/test_removal_onload.html | 142 + image/test/mochitest/test_short_gif_header.html | 35 + image/test/mochitest/test_staticClone.html | 41 + image/test/mochitest/test_svg_animatedGIF.html | 53 + .../test/mochitest/test_svg_filter_animation.html | 42 + .../mochitest/test_synchronized_animation.html | 128 + image/test/mochitest/test_undisplayed_iframe.html | 47 + image/test/mochitest/test_webcam.html | 68 + image/test/mochitest/test_xultree_animation.xhtml | 67 + image/test/mochitest/transparent.gif | Bin 0 -> 355 bytes image/test/mochitest/transparent.png | Bin 0 -> 419 bytes image/test/mochitest/webcam-simulacrum.sjs | 51 + image/test/reftest/ImageDocument.css | 16 + image/test/reftest/apng/bug411852-1-ref.html | 6 + image/test/reftest/apng/bug411852-1-ref.png | Bin 0 -> 164 bytes image/test/reftest/apng/bug411852-1.png | Bin 0 -> 606 bytes image/test/reftest/apng/bug546272-ref.html | 6 + image/test/reftest/apng/bug546272-ref.png | Bin 0 -> 712 bytes image/test/reftest/apng/bug546272.png | Bin 0 -> 1391 bytes image/test/reftest/apng/delaytest.html | 58 + image/test/reftest/apng/reftest.list | 6 + image/test/reftest/avif/1-normal.avif | Bin 0 -> 370 bytes .../test/reftest/avif/2-flipped-horizontally.avif | Bin 0 -> 352 bytes image/test/reftest/avif/3-rotated-180deg.avif | Bin 0 -> 367 bytes image/test/reftest/avif/4-flipped-vertically.avif | Bin 0 -> 359 bytes ...5-rotated-90deg-CCW-and-flipped-vertically.avif | Bin 0 -> 368 bytes image/test/reftest/avif/6-rotated-90deg-CCW.avif | Bin 0 -> 354 bytes .../7-rotated-90deg-CW-and-flipped-vertically.avif | Bin 0 -> 361 bytes image/test/reftest/avif/8-rotated-90deg-CW.avif | Bin 0 -> 359 bytes image/test/reftest/avif/img_irot0_imir0.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot0_imir1.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot0_imirN.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irot1_imir0.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot1_imir1.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot1_imirN.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irot2_imir0.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot2_imir1.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot2_imirN.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irot3_imir0.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot3_imir1.avif | Bin 0 -> 390 bytes image/test/reftest/avif/img_irot3_imirN.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irotN_imir0.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irotN_imir1.avif | Bin 0 -> 380 bytes image/test/reftest/avif/img_irotN_imirN.avif | Bin 0 -> 370 bytes image/test/reftest/avif/reftest.list | 17 + image/test/reftest/bmp/1240629-1.bmp | Bin 0 -> 68 bytes image/test/reftest/bmp/1240629-2.bmp | Bin 0 -> 68 bytes .../reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.bmp | Bin 0 -> 130 bytes .../reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.png | Bin 0 -> 147 bytes .../reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.bmp | Bin 0 -> 122 bytes .../reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.png | Bin 0 -> 220 bytes .../reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.bmp | Bin 0 -> 126 bytes .../reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.png | Bin 0 -> 242 bytes .../reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.bmp | Bin 0 -> 130 bytes .../reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.png | Bin 0 -> 247 bytes .../reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.bmp | Bin 0 -> 66 bytes .../reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.ico | Bin 0 -> 78 bytes .../reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.png | Bin 0 -> 120 bytes .../reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.bmp | Bin 0 -> 70 bytes .../reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.png | Bin 0 -> 126 bytes .../reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.bmp | Bin 0 -> 186 bytes .../reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.png | Bin 0 -> 447 bytes .../reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.bmp | Bin 0 -> 190 bytes .../reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.png | Bin 0 -> 455 bytes .../reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.bmp | Bin 0 -> 326 bytes .../reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.png | Bin 0 -> 489 bytes .../reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.bmp | Bin 0 -> 74 bytes .../reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.png | Bin 0 -> 132 bytes .../reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.bmp | Bin 0 -> 78 bytes .../reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.png | Bin 0 -> 135 bytes .../reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.bmp | Bin 0 -> 82 bytes .../reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.png | Bin 0 -> 146 bytes .../reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.bmp | Bin 0 -> 86 bytes .../reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.png | Bin 0 -> 149 bytes .../reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.bmp | Bin 0 -> 90 bytes .../reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.png | Bin 0 -> 156 bytes .../reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.bmp | Bin 0 -> 94 bytes .../reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.png | Bin 0 -> 161 bytes .../reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.bmp | Bin 0 -> 98 bytes .../reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.png | Bin 0 -> 171 bytes .../bmp/bmp-1bpp/os2bmp-size-32x32-1bpp.bmp | Bin 0 -> 160 bytes image/test/reftest/bmp/bmp-1bpp/reftest.list | 21 + .../bmp/bmp-1bpp/top-to-bottom-16x16-1bpp.bmp | Bin 0 -> 126 bytes .../reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.bmp | Bin 0 -> 802 bytes .../reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.png | Bin 0 -> 490 bytes .../reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.bmp | Bin 0 -> 774 bytes .../reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.png | Bin 0 -> 809 bytes .../reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.bmp | Bin 0 -> 822 bytes .../reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.png | Bin 0 -> 879 bytes .../reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.bmp | Bin 0 -> 938 bytes .../reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.png | Bin 0 -> 1000 bytes .../reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.bmp | Bin 0 -> 58 bytes .../reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.png | Bin 0 -> 70 bytes .../reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.bmp | Bin 0 -> 70 bytes .../reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.png | Bin 0 -> 83 bytes .../reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.bmp | Bin 0 -> 3030 bytes .../reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.png | Bin 0 -> 2936 bytes .../reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.bmp | Bin 0 -> 3126 bytes .../reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.png | Bin 0 -> 3106 bytes .../reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.bmp | Bin 0 -> 3354 bytes .../reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.png | Bin 0 -> 3303 bytes .../reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.bmp | Bin 0 -> 90 bytes .../reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.png | Bin 0 -> 107 bytes .../reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.bmp | Bin 0 -> 102 bytes .../reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.png | Bin 0 -> 136 bytes .../reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.bmp | Bin 0 -> 134 bytes .../reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.png | Bin 0 -> 173 bytes .../reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.bmp | Bin 0 -> 174 bytes .../reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.png | Bin 0 -> 218 bytes .../reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.bmp | Bin 0 -> 222 bytes .../reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.png | Bin 0 -> 271 bytes .../reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.bmp | Bin 0 -> 246 bytes .../reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.png | Bin 0 -> 313 bytes .../reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.bmp | Bin 0 -> 306 bytes .../reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.png | Bin 0 -> 368 bytes .../bmp/bmp-24bpp/os2bmp-size-32x32-24bpp.bmp | Bin 0 -> 3098 bytes image/test/reftest/bmp/bmp-24bpp/reftest.list | 21 + .../bmp/bmp-24bpp/top-to-bottom-16x16-24bpp.bmp | Bin 0 -> 822 bytes .../reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.bmp | Bin 0 -> 254 bytes .../reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.png | Bin 0 -> 229 bytes .../reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.bmp | Bin 0 -> 238 bytes .../reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.png | Bin 0 -> 304 bytes .../reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.bmp | Bin 0 -> 246 bytes .../reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.png | Bin 0 -> 323 bytes .../reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.bmp | Bin 0 -> 322 bytes .../reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.png | Bin 0 -> 337 bytes .../reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.bmp | Bin 0 -> 122 bytes .../reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.png | Bin 0 -> 120 bytes .../reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.bmp | Bin 0 -> 126 bytes .../reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.png | Bin 0 -> 128 bytes .../reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.bmp | Bin 0 -> 614 bytes .../reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.png | Bin 0 -> 700 bytes .../reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.bmp | Bin 0 -> 630 bytes .../reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.png | Bin 0 -> 763 bytes .../reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.bmp | Bin 0 -> 778 bytes .../reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.png | Bin 0 -> 778 bytes .../reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.bmp | Bin 0 -> 130 bytes .../reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.png | Bin 0 -> 139 bytes .../reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.bmp | Bin 0 -> 134 bytes .../reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.png | Bin 0 -> 147 bytes .../reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.bmp | Bin 0 -> 138 bytes .../reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.png | Bin 0 -> 156 bytes .../reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.bmp | Bin 0 -> 142 bytes .../reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.png | Bin 0 -> 163 bytes .../reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.bmp | Bin 0 -> 146 bytes .../reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.png | Bin 0 -> 172 bytes .../reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.bmp | Bin 0 -> 150 bytes .../reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.png | Bin 0 -> 188 bytes .../reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.bmp | Bin 0 -> 190 bytes .../reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.png | Bin 0 -> 198 bytes .../bmp/bmp-4bpp/os2bmp-size-32x32-4bpp.bmp | Bin 0 -> 586 bytes image/test/reftest/bmp/bmp-4bpp/reftest.list | 24 + .../reftest/bmp/bmp-4bpp/rle4-delta-320x240.bmp | Bin 0 -> 3686 bytes .../reftest/bmp/bmp-4bpp/rle4-delta-320x240.png | Bin 0 -> 886 bytes .../bmp/bmp-4bpp/top-to-bottom-16x16-4bpp.bmp | Bin 0 -> 246 bytes .../reftest/bmp/bmp-8bpp/bmp-extrapad-8bpp.png | Bin 0 -> 685 bytes .../reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.bmp | Bin 0 -> 1350 bytes .../reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.png | Bin 0 -> 324 bytes .../reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.bmp | Bin 0 -> 1318 bytes .../reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.png | Bin 0 -> 325 bytes .../reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.bmp | Bin 0 -> 1334 bytes .../reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.png | Bin 0 -> 338 bytes .../reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.bmp | Bin 0 -> 1418 bytes .../reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.png | Bin 0 -> 372 bytes .../reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.bmp | Bin 0 -> 1082 bytes .../reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.png | Bin 0 -> 120 bytes .../reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.bmp | Bin 0 -> 1086 bytes .../reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.png | Bin 0 -> 131 bytes .../reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.bmp | Bin 0 -> 2102 bytes .../reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.png | Bin 0 -> 772 bytes .../reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.bmp | Bin 0 -> 2102 bytes .../reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.png | Bin 0 -> 754 bytes .../reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.bmp | Bin 0 -> 2266 bytes .../reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.png | Bin 0 -> 833 bytes .../reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.bmp | Bin 0 -> 1090 bytes .../reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.png | Bin 0 -> 150 bytes .../reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.bmp | Bin 0 -> 1094 bytes .../reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.png | Bin 0 -> 165 bytes .../reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.bmp | Bin 0 -> 1118 bytes .../reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.png | Bin 0 -> 169 bytes .../reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.bmp | Bin 0 -> 1126 bytes .../reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.png | Bin 0 -> 180 bytes .../reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.bmp | Bin 0 -> 1134 bytes .../reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.png | Bin 0 -> 194 bytes .../reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.bmp | Bin 0 -> 1142 bytes .../reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.png | Bin 0 -> 217 bytes .../reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.bmp | Bin 0 -> 1186 bytes .../reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.png | Bin 0 -> 229 bytes .../bmp/bmp-8bpp/os2-bmp-size-32x32-8bpp.bmp | Bin 0 -> 1818 bytes image/test/reftest/bmp/bmp-8bpp/reftest.list | 25 + .../reftest/bmp/bmp-8bpp/rle-bmp-extrapad-8bpp.bmp | Bin 0 -> 1604 bytes .../bmp/bmp-8bpp/rle-bmp-not-square-8bpp.bmp | Bin 0 -> 1384 bytes .../bmp/bmp-8bpp/rle-bmp-size-32x32-8bpp.bmp | Bin 0 -> 1288 bytes .../bmp/bmp-8bpp/top-to-bottom-16x16-8bpp.bmp | Bin 0 -> 1334 bytes .../top-to-bottom-rle-bmp-size-32x32-8bpp.bmp | Bin 0 -> 1284 bytes .../test/reftest/bmp/bmp-corrupted/invalid-bpp.bmp | Bin 0 -> 58 bytes .../invalid-compression-BITFIELDS.bmp | Bin 0 -> 78 bytes .../bmp/bmp-corrupted/invalid-compression-RLE4.bmp | Bin 0 -> 246 bytes .../bmp/bmp-corrupted/invalid-compression-RLE8.bmp | Bin 0 -> 246 bytes .../bmp/bmp-corrupted/invalid-compression.bmp | Bin 0 -> 822 bytes .../bmp/bmp-corrupted/invalid-data-offset.bmp | Bin 0 -> 3126 bytes .../bmp/bmp-corrupted/invalid-signature.bmp | Bin 0 -> 58 bytes .../bmp-corrupted/invalid-truncated-metadata.bmp | Bin 0 -> 54 bytes .../reftest/bmp/bmp-corrupted/os2-invalid-bpp.bmp | Bin 0 -> 30 bytes image/test/reftest/bmp/bmp-corrupted/reftest.list | 21 + image/test/reftest/bmp/bmp-corrupted/wrapper.html | 28 + image/test/reftest/bmp/bmpsuite/COPYING.txt | 675 ++++ image/test/reftest/bmp/bmpsuite/README.mozilla | 40 + image/test/reftest/bmp/bmpsuite/b/badbitcount.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/badbitssize.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/baddens1.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/baddens2.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/badfilesize.bmp | Bin 0 -> 1086 bytes .../test/reftest/bmp/bmpsuite/b/badheadersize.bmp | Bin 0 -> 1112 bytes .../test/reftest/bmp/bmpsuite/b/badpalettesize.bmp | Bin 0 -> 9254 bytes image/test/reftest/bmp/bmpsuite/b/badplanes.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/badrle.bmp | Bin 0 -> 9212 bytes image/test/reftest/bmp/bmpsuite/b/badrle.png | Bin 0 -> 438 bytes image/test/reftest/bmp/bmpsuite/b/badrle4.bmp | Bin 0 -> 4326 bytes image/test/reftest/bmp/bmpsuite/b/badrle4.png | Bin 0 -> 245 bytes image/test/reftest/bmp/bmpsuite/b/badrle4bis.bmp | Bin 0 -> 4326 bytes image/test/reftest/bmp/bmpsuite/b/badrle4bis.png | Bin 0 -> 880 bytes image/test/reftest/bmp/bmpsuite/b/badrle4ter.bmp | Bin 0 -> 4326 bytes image/test/reftest/bmp/bmpsuite/b/badrle4ter.png | Bin 0 -> 883 bytes image/test/reftest/bmp/bmpsuite/b/badrlebis.bmp | Bin 0 -> 9212 bytes image/test/reftest/bmp/bmpsuite/b/badrlebis.png | Bin 0 -> 1626 bytes image/test/reftest/bmp/bmpsuite/b/badrleter.bmp | Bin 0 -> 9212 bytes image/test/reftest/bmp/bmpsuite/b/badrleter.png | Bin 0 -> 1628 bytes image/test/reftest/bmp/bmpsuite/b/badwidth.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/b/pal1.png | Bin 0 -> 586 bytes image/test/reftest/bmp/bmpsuite/b/pal8.png | Bin 0 -> 3772 bytes image/test/reftest/bmp/bmpsuite/b/pal8badindex.bmp | Bin 0 -> 8650 bytes image/test/reftest/bmp/bmpsuite/b/pal8badindex.png | Bin 0 -> 1819 bytes image/test/reftest/bmp/bmpsuite/b/reallybig.bmp | Bin 0 -> 24630 bytes image/test/reftest/bmp/bmpsuite/b/reftest.list | 110 + image/test/reftest/bmp/bmpsuite/b/rgb16-880.bmp | Bin 0 -> 16450 bytes image/test/reftest/bmp/bmpsuite/b/rgb16-880.png | Bin 0 -> 1029 bytes image/test/reftest/bmp/bmpsuite/b/rletopdown.bmp | Bin 0 -> 8788 bytes image/test/reftest/bmp/bmpsuite/b/shortfile.bmp | Bin 0 -> 273 bytes image/test/reftest/bmp/bmpsuite/b/shortfile.png | Bin 0 -> 399 bytes image/test/reftest/bmp/bmpsuite/b/wrapper.html | 28 + image/test/reftest/bmp/bmpsuite/g/pal1.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/g/pal1.png | Bin 0 -> 586 bytes image/test/reftest/bmp/bmpsuite/g/pal1bg.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/g/pal1bg.png | Bin 0 -> 604 bytes image/test/reftest/bmp/bmpsuite/g/pal1wb.bmp | Bin 0 -> 1086 bytes image/test/reftest/bmp/bmpsuite/g/pal4.bmp | Bin 0 -> 4198 bytes image/test/reftest/bmp/bmpsuite/g/pal4.png | Bin 0 -> 1428 bytes image/test/reftest/bmp/bmpsuite/g/pal4gs.bmp | Bin 0 -> 4198 bytes image/test/reftest/bmp/bmpsuite/g/pal4gs.png | Bin 0 -> 2016 bytes image/test/reftest/bmp/bmpsuite/g/pal4rle.bmp | Bin 0 -> 3836 bytes image/test/reftest/bmp/bmpsuite/g/pal8-0.bmp | Bin 0 -> 9270 bytes image/test/reftest/bmp/bmpsuite/g/pal8.bmp | Bin 0 -> 9254 bytes image/test/reftest/bmp/bmpsuite/g/pal8.png | Bin 0 -> 3772 bytes image/test/reftest/bmp/bmpsuite/g/pal8gs.bmp | Bin 0 -> 9254 bytes image/test/reftest/bmp/bmpsuite/g/pal8gs.png | Bin 0 -> 9441 bytes .../reftest/bmp/bmpsuite/g/pal8nonsquare-e.png | Bin 0 -> 2513 bytes .../test/reftest/bmp/bmpsuite/g/pal8nonsquare.bmp | Bin 0 -> 5158 bytes .../test/reftest/bmp/bmpsuite/g/pal8nonsquare.png | Bin 0 -> 2714 bytes image/test/reftest/bmp/bmpsuite/g/pal8os2.bmp | Bin 0 -> 8986 bytes image/test/reftest/bmp/bmpsuite/g/pal8rle.bmp | Bin 0 -> 8788 bytes image/test/reftest/bmp/bmpsuite/g/pal8topdown.bmp | Bin 0 -> 9254 bytes image/test/reftest/bmp/bmpsuite/g/pal8v4.bmp | Bin 0 -> 9322 bytes image/test/reftest/bmp/bmpsuite/g/pal8v5.bmp | Bin 0 -> 9338 bytes image/test/reftest/bmp/bmpsuite/g/pal8w124.bmp | Bin 0 -> 8626 bytes image/test/reftest/bmp/bmpsuite/g/pal8w124.png | Bin 0 -> 3585 bytes image/test/reftest/bmp/bmpsuite/g/pal8w125.bmp | Bin 0 -> 8998 bytes image/test/reftest/bmp/bmpsuite/g/pal8w125.png | Bin 0 -> 3628 bytes image/test/reftest/bmp/bmpsuite/g/pal8w126.bmp | Bin 0 -> 9126 bytes image/test/reftest/bmp/bmpsuite/g/pal8w126.png | Bin 0 -> 3714 bytes image/test/reftest/bmp/bmpsuite/g/reftest.list | 129 + image/test/reftest/bmp/bmpsuite/g/rgb16-565.bmp | Bin 0 -> 16450 bytes image/test/reftest/bmp/bmpsuite/g/rgb16-565.png | Bin 0 -> 1297 bytes image/test/reftest/bmp/bmpsuite/g/rgb16-565pal.bmp | Bin 0 -> 17474 bytes image/test/reftest/bmp/bmpsuite/g/rgb16.bmp | Bin 0 -> 16438 bytes image/test/reftest/bmp/bmpsuite/g/rgb16.png | Bin 0 -> 1177 bytes image/test/reftest/bmp/bmpsuite/g/rgb16bfdef.bmp | Bin 0 -> 16450 bytes image/test/reftest/bmp/bmpsuite/g/rgb24.bmp | Bin 0 -> 24630 bytes image/test/reftest/bmp/bmpsuite/g/rgb24.png | Bin 0 -> 1072 bytes image/test/reftest/bmp/bmpsuite/g/rgb24pal.bmp | Bin 0 -> 25654 bytes image/test/reftest/bmp/bmpsuite/g/rgb32.bmp | Bin 0 -> 32566 bytes image/test/reftest/bmp/bmpsuite/g/rgb32bf.bmp | Bin 0 -> 32578 bytes image/test/reftest/bmp/bmpsuite/g/rgb32bfdef.bmp | Bin 0 -> 32578 bytes image/test/reftest/bmp/bmpsuite/q/pal1huff.bmp | Bin 0 -> 2151 bytes image/test/reftest/bmp/bmpsuite/q/pal1p1.bmp | Bin 0 -> 1082 bytes image/test/reftest/bmp/bmpsuite/q/pal1p1.png | Bin 0 -> 124 bytes image/test/reftest/bmp/bmpsuite/q/pal2.bmp | Bin 0 -> 2118 bytes image/test/reftest/bmp/bmpsuite/q/pal2color.bmp | Bin 0 -> 2118 bytes image/test/reftest/bmp/bmpsuite/q/pal4rlecut.bmp | Bin 0 -> 3610 bytes image/test/reftest/bmp/bmpsuite/q/pal4rlecut.png | Bin 0 -> 1918 bytes image/test/reftest/bmp/bmpsuite/q/pal4rletrns.bmp | Bin 0 -> 4326 bytes image/test/reftest/bmp/bmpsuite/q/pal4rletrns.png | Bin 0 -> 1465 bytes image/test/reftest/bmp/bmpsuite/q/pal8.png | Bin 0 -> 3772 bytes image/test/reftest/bmp/bmpsuite/q/pal8offs.bmp | Bin 0 -> 9354 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2-hs.bmp | Bin 0 -> 8986 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2-sz.bmp | Bin 0 -> 8986 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2sp.bmp | Bin 0 -> 8974 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2v2-16.bmp | Bin 0 -> 9246 bytes .../test/reftest/bmp/bmpsuite/q/pal8os2v2-40sz.bmp | Bin 0 -> 9254 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2v2-sz.bmp | Bin 0 -> 9278 bytes image/test/reftest/bmp/bmpsuite/q/pal8os2v2.bmp | Bin 0 -> 9278 bytes .../reftest/bmp/bmpsuite/q/pal8oversizepal.bmp | Bin 0 -> 9446 bytes image/test/reftest/bmp/bmpsuite/q/pal8rlecut.bmp | Bin 0 -> 7980 bytes image/test/reftest/bmp/bmpsuite/q/pal8rlecut.png | Bin 0 -> 3524 bytes image/test/reftest/bmp/bmpsuite/q/pal8rletrns.bmp | Bin 0 -> 9212 bytes image/test/reftest/bmp/bmpsuite/q/pal8rletrns.png | Bin 0 -> 3793 bytes image/test/reftest/bmp/bmpsuite/q/reftest.list | 251 ++ image/test/reftest/bmp/bmpsuite/q/rgb16-231.bmp | Bin 0 -> 16450 bytes image/test/reftest/bmp/bmpsuite/q/rgb16-231.png | Bin 0 -> 2643 bytes image/test/reftest/bmp/bmpsuite/q/rgb16-3103.bmp | Bin 0 -> 16450 bytes image/test/reftest/bmp/bmpsuite/q/rgb16-3103.png | Bin 0 -> 3347 bytes image/test/reftest/bmp/bmpsuite/q/rgb16.png | Bin 0 -> 1177 bytes .../test/reftest/bmp/bmpsuite/q/rgb16faketrns.bmp | Bin 0 -> 16438 bytes image/test/reftest/bmp/bmpsuite/q/rgb24.png | Bin 0 -> 1072 bytes image/test/reftest/bmp/bmpsuite/q/rgb24jpeg.bmp | Bin 0 -> 2457 bytes .../test/reftest/bmp/bmpsuite/q/rgb24largepal.bmp | Bin 0 -> 25830 bytes image/test/reftest/bmp/bmpsuite/q/rgb24lprof.bmp | Bin 0 -> 24743 bytes image/test/reftest/bmp/bmpsuite/q/rgb24png.bmp | Bin 0 -> 1210 bytes image/test/reftest/bmp/bmpsuite/q/rgb24prof.bmp | Bin 0 -> 27782 bytes image/test/reftest/bmp/bmpsuite/q/rgb24prof2.bmp | Bin 0 -> 25254 bytes image/test/reftest/bmp/bmpsuite/q/rgb24rle24.bmp | Bin 0 -> 21432 bytes image/test/reftest/bmp/bmpsuite/q/rgb32-111110.bmp | Bin 0 -> 32578 bytes image/test/reftest/bmp/bmpsuite/q/rgb32-7187.bmp | Bin 0 -> 32578 bytes image/test/reftest/bmp/bmpsuite/q/rgb32-7187.png | Bin 0 -> 2136 bytes image/test/reftest/bmp/bmpsuite/q/rgb32-xbgr.bmp | Bin 0 -> 32650 bytes .../test/reftest/bmp/bmpsuite/q/rgb32fakealpha.bmp | Bin 0 -> 32566 bytes image/test/reftest/bmp/bmpsuite/q/rgb32h52.bmp | Bin 0 -> 32578 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-1924.bmp | Bin 0 -> 16522 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-1924.png | Bin 0 -> 2811 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-4444.bmp | Bin 0 -> 16522 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-4444.png | Bin 0 -> 1093 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-5551.bmp | Bin 0 -> 16522 bytes image/test/reftest/bmp/bmpsuite/q/rgba16-5551.png | Bin 0 -> 1226 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-1.bmp | Bin 0 -> 32650 bytes .../test/reftest/bmp/bmpsuite/q/rgba32-1010102.bmp | Bin 0 -> 32650 bytes .../test/reftest/bmp/bmpsuite/q/rgba32-1010102.png | Bin 0 -> 1253 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-2.bmp | Bin 0 -> 32650 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-61754.bmp | Bin 0 -> 32650 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-61754.png | Bin 0 -> 2483 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-81284.bmp | Bin 0 -> 32650 bytes image/test/reftest/bmp/bmpsuite/q/rgba32-81284.png | Bin 0 -> 2182 bytes image/test/reftest/bmp/bmpsuite/q/rgba32.png | Bin 0 -> 1229 bytes image/test/reftest/bmp/bmpsuite/q/rgba32abf.bmp | Bin 0 -> 32582 bytes image/test/reftest/bmp/bmpsuite/q/rgba32h56.bmp | Bin 0 -> 32582 bytes image/test/reftest/bmp/bmpsuite/q/wrapper.html | 28 + image/test/reftest/bmp/bmpsuite/reftest.list | 8 + image/test/reftest/bmp/bmpsuite/x/ba-bm.bmp | Bin 0 -> 9000 bytes image/test/reftest/bmp/bmpsuite/x/reftest.list | 10 + image/test/reftest/bmp/bmpsuite/x/wrapper.html | 28 + image/test/reftest/bmp/reftest.list | 16 + image/test/reftest/color-management/color-curv.png | Bin 0 -> 1753 bytes image/test/reftest/color-management/color-lin.png | Bin 0 -> 1749 bytes .../test/reftest/color-management/color-table.png | Bin 0 -> 1754 bytes .../reftest/color-management/invalid-chrm-ref.png | Bin 0 -> 1460 bytes .../test/reftest/color-management/invalid-chrm.png | Bin 0 -> 1504 bytes .../color-management/invalid-whitepoint.png | Bin 0 -> 1504 bytes image/test/reftest/color-management/reftest.list | 7 + .../reftest/color-management/trc-type-ref.html | 8 + image/test/reftest/color-management/trc-type.html | 53 + image/test/reftest/colordepth.html | 16 + image/test/reftest/downscaling/100x100.gif | Bin 0 -> 956 bytes image/test/reftest/downscaling/100x100.jpg | Bin 0 -> 917 bytes image/test/reftest/downscaling/100x100.png | Bin 0 -> 338 bytes image/test/reftest/downscaling/100x32768.gif | Bin 0 -> 4299 bytes image/test/reftest/downscaling/100x32768.jpg | Bin 0 -> 22041 bytes image/test/reftest/downscaling/100x32768.png | Bin 0 -> 70926 bytes image/test/reftest/downscaling/1404366-1.html | 14 + image/test/reftest/downscaling/1404366-1.ico | Bin 0 -> 4287 bytes image/test/reftest/downscaling/1421191-1.html | 20 + image/test/reftest/downscaling/1421191-1.png | Bin 0 -> 92182 bytes image/test/reftest/downscaling/32768x100.gif | Bin 0 -> 4299 bytes image/test/reftest/downscaling/32768x100.jpg | Bin 0 -> 58065 bytes image/test/reftest/downscaling/32768x100.png | Bin 0 -> 13172 bytes .../reftest/downscaling/black-border-bottom.png | Bin 0 -> 4094 bytes .../test/reftest/downscaling/black-border-left.png | Bin 0 -> 4176 bytes .../test/reftest/downscaling/black-border-rect.svg | 3 + .../reftest/downscaling/black-border-right.png | Bin 0 -> 4097 bytes .../test/reftest/downscaling/black-border-top.png | Bin 0 -> 4144 bytes .../reftest/downscaling/bmp-size-16x16-24bpp.png | Bin 0 -> 879 bytes .../reftest/downscaling/downscale-1-bigimage.png | Bin 0 -> 195 bytes .../test/reftest/downscaling/downscale-1-ref.html | 8 + .../reftest/downscaling/downscale-1-smallimage.png | Bin 0 -> 88 bytes image/test/reftest/downscaling/downscale-1.html | 24 + image/test/reftest/downscaling/downscale-16px.html | 28 + image/test/reftest/downscaling/downscale-2a.html | 31 + image/test/reftest/downscaling/downscale-2b.html | 31 + image/test/reftest/downscaling/downscale-2c.html | 31 + image/test/reftest/downscaling/downscale-2d.html | 31 + image/test/reftest/downscaling/downscale-2e.html | 31 + image/test/reftest/downscaling/downscale-2f.html | 31 + .../reftest/downscaling/downscale-32px-ref.html | 8 + image/test/reftest/downscaling/downscale-32px.html | 31 + image/test/reftest/downscaling/downscale-8px.html | 27 + .../downscaling/downscale-moz-icon-1-ref.html | 41 + .../reftest/downscaling/downscale-moz-icon-1.html | 19 + .../reftest/downscaling/downscale-orient-ref.html | 24 + .../reftest/downscaling/downscale-orient-ref.png | Bin 0 -> 146 bytes .../test/reftest/downscaling/downscale-orient.html | 24 + image/test/reftest/downscaling/downscale-png.html | 31 + .../reftest/downscaling/downscale-svg-1-ref.html | 13 + .../test/reftest/downscaling/downscale-svg-1a.html | 8 + .../test/reftest/downscaling/downscale-svg-1b.html | 8 + .../test/reftest/downscaling/downscale-svg-1c.html | 8 + .../test/reftest/downscaling/downscale-svg-1d.html | 8 + .../test/reftest/downscaling/downscale-svg-1e.html | 8 + .../test/reftest/downscaling/downscale-svg-1f.html | 8 + image/test/reftest/downscaling/ff-0RGB.ico | Bin 0 -> 4286 bytes image/test/reftest/downscaling/ff-0RGB.png | Bin 0 -> 2515 bytes image/test/reftest/downscaling/ff-ARGB.ico | Bin 0 -> 4286 bytes image/test/reftest/downscaling/ff-ARGB.png | Bin 0 -> 115 bytes image/test/reftest/downscaling/huge-1.html | 9 + .../downscaling/image-pre-rotated-90-deg.jpg | Bin 0 -> 6914 bytes .../reftest/downscaling/lime-red-256px-bmp-in.ico | Bin 0 -> 74814 bytes .../reftest/downscaling/lime-red-256px-png-in.ico | Bin 0 -> 881 bytes image/test/reftest/downscaling/lime-red-256px.bmp | Bin 0 -> 196730 bytes image/test/reftest/downscaling/lime-red-256px.gif | Bin 0 -> 873 bytes image/test/reftest/downscaling/lime-red-256px.jpg | Bin 0 -> 2865 bytes image/test/reftest/downscaling/lime-red-256px.png | Bin 0 -> 568 bytes image/test/reftest/downscaling/lime-red-256px.svg | 5 + image/test/reftest/downscaling/lime-red-32px.png | Bin 0 -> 103 bytes image/test/reftest/downscaling/png-interlaced.png | Bin 0 -> 806 bytes image/test/reftest/downscaling/png-normal.png | Bin 0 -> 421 bytes image/test/reftest/downscaling/reftest.list | 217 ++ .../downscaling/top-to-bottom-16x16-24bpp.bmp | Bin 0 -> 822 bytes .../reftest/encoders-lossless/ImageDocument.css | 16 + image/test/reftest/encoders-lossless/encoder.html | 113 + image/test/reftest/encoders-lossless/reftest.list | 175 + .../test/reftest/encoders-lossless/size-15x15.png | Bin 0 -> 809 bytes .../test/reftest/encoders-lossless/size-16x16.png | Bin 0 -> 879 bytes .../test/reftest/encoders-lossless/size-17x17.png | Bin 0 -> 1000 bytes image/test/reftest/encoders-lossless/size-1x1.png | Bin 0 -> 70 bytes .../reftest/encoders-lossless/size-256x256.png | Bin 0 -> 5480 bytes image/test/reftest/encoders-lossless/size-2x2.png | Bin 0 -> 83 bytes .../test/reftest/encoders-lossless/size-31x31.png | Bin 0 -> 2936 bytes .../test/reftest/encoders-lossless/size-32x32.png | Bin 0 -> 3106 bytes .../test/reftest/encoders-lossless/size-33x33.png | Bin 0 -> 3303 bytes image/test/reftest/encoders-lossless/size-3x3.png | Bin 0 -> 107 bytes image/test/reftest/encoders-lossless/size-4x4.png | Bin 0 -> 136 bytes image/test/reftest/encoders-lossless/size-5x5.png | Bin 0 -> 173 bytes image/test/reftest/encoders-lossless/size-6x6.png | Bin 0 -> 218 bytes image/test/reftest/encoders-lossless/size-7x7.png | Bin 0 -> 271 bytes image/test/reftest/encoders-lossless/size-8x8.png | Bin 0 -> 313 bytes image/test/reftest/encoders-lossless/size-9x9.png | Bin 0 -> 368 bytes image/test/reftest/encoders-lossless/test.png | Bin 0 -> 3106 bytes .../reftest/generic/accept-image-catchall-ref.html | 12 + .../reftest/generic/accept-image-catchall.html | 13 + image/test/reftest/generic/check-header.sjs | 72 + image/test/reftest/generic/green.png | Bin 0 -> 201 bytes image/test/reftest/generic/moz-icon-1.html | 1 + .../generic/moz-icon-blank-1-almostref.html | 2 + .../reftest/generic/moz-icon-blank-1-antiref.html | 2 + .../reftest/generic/moz-icon-blank-1-antiref2.html | 2 + .../test/reftest/generic/moz-icon-blank-1-ref.html | 2 + image/test/reftest/generic/moz-icon-blank-1.html | 2 + image/test/reftest/generic/reftest.list | 6 + image/test/reftest/gif/1bit-255-trans.gif | Bin 0 -> 337 bytes image/test/reftest/gif/1bit-255-trans.png | Bin 0 -> 1214 bytes image/test/reftest/gif/ImageDocument.css | 16 + image/test/reftest/gif/animation1a.gif | Bin 0 -> 167 bytes image/test/reftest/gif/animation2a-finalframe.gif | Bin 0 -> 107 bytes image/test/reftest/gif/animation2a.gif | Bin 0 -> 167 bytes image/test/reftest/gif/blue.gif | Bin 0 -> 43 bytes image/test/reftest/gif/comment.gif | Bin 0 -> 68 bytes image/test/reftest/gif/comment.png | Bin 0 -> 167 bytes image/test/reftest/gif/delaytest.html | 58 + image/test/reftest/gif/in-colormap-trans.gif | Bin 0 -> 355 bytes image/test/reftest/gif/in-colormap-trans.png | Bin 0 -> 237 bytes image/test/reftest/gif/one-color-offset-ref.gif | Bin 0 -> 69 bytes image/test/reftest/gif/one-color-offset.gif | Bin 0 -> 49 bytes .../reftest/gif/one-pixel-no-image-data-ref.html | 11 + .../test/reftest/gif/one-pixel-no-image-data.html | 11 + image/test/reftest/gif/out-of-colormap-trans.gif | Bin 0 -> 355 bytes image/test/reftest/gif/out-of-colormap-trans.png | Bin 0 -> 241 bytes image/test/reftest/gif/red.gif | Bin 0 -> 43 bytes image/test/reftest/gif/reftest.list | 32 + .../reftest/gif/small-background-size-2-ref.gif | Bin 0 -> 807 bytes image/test/reftest/gif/small-background-size-2.gif | Bin 0 -> 572 bytes .../test/reftest/gif/small-background-size-ref.gif | Bin 0 -> 1076 bytes image/test/reftest/gif/small-background-size.gif | Bin 0 -> 991 bytes image/test/reftest/gif/test_bug641198.html | 53 + image/test/reftest/gif/tile-transform-ref.html | 12 + image/test/reftest/gif/tile-transform.html | 12 + image/test/reftest/gif/tiletest-ref.png | Bin 0 -> 282 bytes image/test/reftest/gif/tiletest.gif | Bin 0 -> 156 bytes .../gif/transparent-animation-finalframe.gif | Bin 0 -> 121 bytes .../gif/transparent-animation-finalframe.html | 6 + image/test/reftest/gif/transparent-animation.gif | Bin 0 -> 527 bytes .../gif/truncated-framerect-interlaced-ref.gif | Bin 0 -> 927 bytes .../reftest/gif/truncated-framerect-interlaced.gif | Bin 0 -> 927 bytes image/test/reftest/gif/truncated-framerect-ref.gif | Bin 0 -> 929 bytes .../test/reftest/gif/truncated-framerect-ref.html | 33 + image/test/reftest/gif/truncated-framerect.gif | Bin 0 -> 929 bytes image/test/reftest/gif/truncated-framerect.html | 28 + image/test/reftest/ico/cur/pointer.cur | Bin 0 -> 4286 bytes image/test/reftest/ico/cur/pointer.png | Bin 0 -> 453 bytes image/test/reftest/ico/cur/reftest.list | 4 + image/test/reftest/ico/cur/wrapper.html | 28 + .../ico-not-square-transparent-1bpp.ico | Bin 0 -> 182 bytes .../ico-not-square-transparent-1bpp.png | Bin 0 -> 241 bytes .../ico-bmp-1bpp/ico-partial-transparent-1bpp.ico | Bin 0 -> 326 bytes .../ico-bmp-1bpp/ico-partial-transparent-1bpp.png | Bin 0 -> 410 bytes .../ico/ico-bmp-1bpp/ico-size-15x15-1bpp.ico | Bin 0 -> 190 bytes .../ico/ico-bmp-1bpp/ico-size-15x15-1bpp.png | Bin 0 -> 220 bytes .../ico/ico-bmp-1bpp/ico-size-16x16-1bpp.ico | Bin 0 -> 198 bytes .../ico/ico-bmp-1bpp/ico-size-16x16-1bpp.png | Bin 0 -> 242 bytes .../ico/ico-bmp-1bpp/ico-size-17x17-1bpp.ico | Bin 0 -> 206 bytes .../ico/ico-bmp-1bpp/ico-size-17x17-1bpp.png | Bin 0 -> 247 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.ico | Bin 0 -> 78 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.png | Bin 0 -> 120 bytes .../ico/ico-bmp-1bpp/ico-size-256x256-1bpp.ico | Bin 0 -> 16454 bytes .../ico/ico-bmp-1bpp/ico-size-256x256-1bpp.png | Bin 0 -> 7673 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.ico | Bin 0 -> 86 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.png | Bin 0 -> 126 bytes .../ico/ico-bmp-1bpp/ico-size-31x31-1bpp.ico | Bin 0 -> 318 bytes .../ico/ico-bmp-1bpp/ico-size-31x31-1bpp.png | Bin 0 -> 447 bytes .../ico/ico-bmp-1bpp/ico-size-32x32-1bpp.ico | Bin 0 -> 326 bytes .../ico/ico-bmp-1bpp/ico-size-32x32-1bpp.png | Bin 0 -> 455 bytes .../ico/ico-bmp-1bpp/ico-size-33x33-1bpp.ico | Bin 0 -> 598 bytes .../ico/ico-bmp-1bpp/ico-size-33x33-1bpp.png | Bin 0 -> 489 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.ico | Bin 0 -> 94 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.png | Bin 0 -> 132 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.ico | Bin 0 -> 102 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.png | Bin 0 -> 135 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.ico | Bin 0 -> 110 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.png | Bin 0 -> 146 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.ico | Bin 0 -> 118 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.png | Bin 0 -> 149 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.ico | Bin 0 -> 126 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.png | Bin 0 -> 156 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.ico | Bin 0 -> 134 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.png | Bin 0 -> 161 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.ico | Bin 0 -> 142 bytes .../reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.png | Bin 0 -> 171 bytes .../ico/ico-bmp-1bpp/ico-transparent-1bpp.ico | Bin 0 -> 3262 bytes .../ico/ico-bmp-1bpp/ico-transparent-1bpp.png | Bin 0 -> 195 bytes image/test/reftest/ico/ico-bmp-1bpp/reftest.list | 23 + .../ico-not-square-transparent-24bpp.ico | Bin 0 -> 1126 bytes .../ico-not-square-transparent-24bpp.png | Bin 0 -> 514 bytes .../ico-partial-transparent-24bpp.ico | Bin 0 -> 3262 bytes .../ico-partial-transparent-24bpp.png | Bin 0 -> 1028 bytes .../ico/ico-bmp-24bpp/ico-size-15x15-24bpp.ico | Bin 0 -> 842 bytes .../ico/ico-bmp-24bpp/ico-size-15x15-24bpp.png | Bin 0 -> 809 bytes .../ico/ico-bmp-24bpp/ico-size-16x16-24bpp.ico | Bin 0 -> 894 bytes .../ico/ico-bmp-24bpp/ico-size-16x16-24bpp.png | Bin 0 -> 879 bytes .../ico/ico-bmp-24bpp/ico-size-17x17-24bpp.ico | Bin 0 -> 1014 bytes .../ico/ico-bmp-24bpp/ico-size-17x17-24bpp.png | Bin 0 -> 1000 bytes .../ico/ico-bmp-24bpp/ico-size-1x1-24bpp.ico | Bin 0 -> 70 bytes .../ico/ico-bmp-24bpp/ico-size-1x1-24bpp.png | Bin 0 -> 70 bytes .../ico/ico-bmp-24bpp/ico-size-256x256-24bpp.ico | Bin 0 -> 204862 bytes .../ico/ico-bmp-24bpp/ico-size-256x256-24bpp.png | Bin 0 -> 5480 bytes .../ico/ico-bmp-24bpp/ico-size-2x2-24bpp.ico | Bin 0 -> 86 bytes .../ico/ico-bmp-24bpp/ico-size-2x2-24bpp.png | Bin 0 -> 83 bytes .../ico/ico-bmp-24bpp/ico-size-31x31-24bpp.ico | Bin 0 -> 3162 bytes .../ico/ico-bmp-24bpp/ico-size-31x31-24bpp.png | Bin 0 -> 2936 bytes .../ico/ico-bmp-24bpp/ico-size-32x32-24bpp.ico | Bin 0 -> 3262 bytes .../ico/ico-bmp-24bpp/ico-size-32x32-24bpp.png | Bin 0 -> 3106 bytes .../ico/ico-bmp-24bpp/ico-size-33x33-24bpp.ico | Bin 0 -> 3626 bytes .../ico/ico-bmp-24bpp/ico-size-33x33-24bpp.png | Bin 0 -> 3303 bytes .../ico/ico-bmp-24bpp/ico-size-3x3-24bpp.ico | Bin 0 -> 110 bytes .../ico/ico-bmp-24bpp/ico-size-3x3-24bpp.png | Bin 0 -> 107 bytes .../ico/ico-bmp-24bpp/ico-size-4x4-24bpp.ico | Bin 0 -> 126 bytes .../ico/ico-bmp-24bpp/ico-size-4x4-24bpp.png | Bin 0 -> 136 bytes .../ico/ico-bmp-24bpp/ico-size-5x5-24bpp.ico | Bin 0 -> 162 bytes .../ico/ico-bmp-24bpp/ico-size-5x5-24bpp.png | Bin 0 -> 173 bytes .../ico/ico-bmp-24bpp/ico-size-6x6-24bpp.ico | Bin 0 -> 206 bytes .../ico/ico-bmp-24bpp/ico-size-6x6-24bpp.png | Bin 0 -> 218 bytes .../ico/ico-bmp-24bpp/ico-size-7x7-24bpp.ico | Bin 0 -> 258 bytes .../ico/ico-bmp-24bpp/ico-size-7x7-24bpp.png | Bin 0 -> 271 bytes .../ico/ico-bmp-24bpp/ico-size-8x8-24bpp.ico | Bin 0 -> 286 bytes .../ico/ico-bmp-24bpp/ico-size-8x8-24bpp.png | Bin 0 -> 313 bytes .../ico/ico-bmp-24bpp/ico-size-9x9-24bpp.ico | Bin 0 -> 350 bytes .../ico/ico-bmp-24bpp/ico-size-9x9-24bpp.png | Bin 0 -> 368 bytes .../ico/ico-bmp-24bpp/ico-transparent-24bpp.ico | Bin 0 -> 3262 bytes .../ico/ico-bmp-24bpp/ico-transparent-24bpp.png | Bin 0 -> 195 bytes image/test/reftest/ico/ico-bmp-24bpp/reftest.list | 23 + .../ico-not-square-transparent-32bpp.ico | Bin 0 -> 1462 bytes .../ico-not-square-transparent-32bpp.png | Bin 0 -> 533 bytes .../ico-partial-transparent-32bpp.ico | Bin 0 -> 4286 bytes .../ico-partial-transparent-32bpp.png | Bin 0 -> 1028 bytes .../ico/ico-bmp-32bpp/ico-size-15x15-32bpp.ico | Bin 0 -> 1022 bytes .../ico/ico-bmp-32bpp/ico-size-15x15-32bpp.png | Bin 0 -> 809 bytes .../ico/ico-bmp-32bpp/ico-size-16x16-32bpp.ico | Bin 0 -> 1150 bytes .../ico/ico-bmp-32bpp/ico-size-16x16-32bpp.png | Bin 0 -> 879 bytes .../ico/ico-bmp-32bpp/ico-size-17x17-32bpp.ico | Bin 0 -> 1286 bytes .../ico/ico-bmp-32bpp/ico-size-17x17-32bpp.png | Bin 0 -> 1000 bytes .../ico/ico-bmp-32bpp/ico-size-1x1-32bpp.ico | Bin 0 -> 70 bytes .../ico/ico-bmp-32bpp/ico-size-1x1-32bpp.png | Bin 0 -> 70 bytes .../ico/ico-bmp-32bpp/ico-size-256x256-32bpp.ico | Bin 0 -> 270398 bytes .../ico/ico-bmp-32bpp/ico-size-256x256-32bpp.png | Bin 0 -> 5480 bytes .../ico/ico-bmp-32bpp/ico-size-2x2-32bpp.ico | Bin 0 -> 86 bytes .../ico/ico-bmp-32bpp/ico-size-2x2-32bpp.png | Bin 0 -> 83 bytes .../ico/ico-bmp-32bpp/ico-size-31x31-32bpp.ico | Bin 0 -> 4030 bytes .../ico/ico-bmp-32bpp/ico-size-31x31-32bpp.png | Bin 0 -> 2936 bytes .../ico/ico-bmp-32bpp/ico-size-32x32-32bpp.ico | Bin 0 -> 4286 bytes .../ico/ico-bmp-32bpp/ico-size-32x32-32bpp.png | Bin 0 -> 3106 bytes .../ico/ico-bmp-32bpp/ico-size-33x33-32bpp.ico | Bin 0 -> 4682 bytes .../ico/ico-bmp-32bpp/ico-size-33x33-32bpp.png | Bin 0 -> 3303 bytes .../ico/ico-bmp-32bpp/ico-size-3x3-32bpp.ico | Bin 0 -> 110 bytes .../ico/ico-bmp-32bpp/ico-size-3x3-32bpp.png | Bin 0 -> 107 bytes .../ico/ico-bmp-32bpp/ico-size-4x4-32bpp.ico | Bin 0 -> 142 bytes .../ico/ico-bmp-32bpp/ico-size-4x4-32bpp.png | Bin 0 -> 136 bytes .../ico/ico-bmp-32bpp/ico-size-5x5-32bpp.ico | Bin 0 -> 182 bytes .../ico/ico-bmp-32bpp/ico-size-5x5-32bpp.png | Bin 0 -> 173 bytes .../ico/ico-bmp-32bpp/ico-size-6x6-32bpp.ico | Bin 0 -> 230 bytes .../ico/ico-bmp-32bpp/ico-size-6x6-32bpp.png | Bin 0 -> 218 bytes .../ico/ico-bmp-32bpp/ico-size-7x7-32bpp.ico | Bin 0 -> 286 bytes .../ico/ico-bmp-32bpp/ico-size-7x7-32bpp.png | Bin 0 -> 271 bytes .../ico/ico-bmp-32bpp/ico-size-8x8-32bpp.ico | Bin 0 -> 350 bytes .../ico/ico-bmp-32bpp/ico-size-8x8-32bpp.png | Bin 0 -> 313 bytes .../ico/ico-bmp-32bpp/ico-size-9x9-32bpp.ico | Bin 0 -> 422 bytes .../ico/ico-bmp-32bpp/ico-size-9x9-32bpp.png | Bin 0 -> 368 bytes .../ico/ico-bmp-32bpp/ico-transparent-32bpp.ico | Bin 0 -> 4286 bytes .../ico/ico-bmp-32bpp/ico-transparent-32bpp.png | Bin 0 -> 195 bytes image/test/reftest/ico/ico-bmp-32bpp/reftest.list | 22 + .../ico-not-square-transparent-4bpp.ico | Bin 0 -> 350 bytes .../ico-not-square-transparent-4bpp.png | Bin 0 -> 315 bytes .../ico-bmp-4bpp/ico-partial-transparent-4bpp.ico | Bin 0 -> 766 bytes .../ico-bmp-4bpp/ico-partial-transparent-4bpp.png | Bin 0 -> 556 bytes .../ico/ico-bmp-4bpp/ico-size-15x15-4bpp.ico | Bin 0 -> 306 bytes .../ico/ico-bmp-4bpp/ico-size-15x15-4bpp.png | Bin 0 -> 304 bytes .../ico/ico-bmp-4bpp/ico-size-16x16-4bpp.ico | Bin 0 -> 318 bytes .../ico/ico-bmp-4bpp/ico-size-16x16-4bpp.png | Bin 0 -> 323 bytes .../ico/ico-bmp-4bpp/ico-size-17x17-4bpp.ico | Bin 0 -> 398 bytes .../ico/ico-bmp-4bpp/ico-size-17x17-4bpp.png | Bin 0 -> 337 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.ico | Bin 0 -> 134 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.png | Bin 0 -> 120 bytes .../ico/ico-bmp-4bpp/ico-size-256x256-4bpp.ico | Bin 0 -> 41086 bytes .../ico/ico-bmp-4bpp/ico-size-256x256-4bpp.png | Bin 0 -> 16944 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.ico | Bin 0 -> 142 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.png | Bin 0 -> 128 bytes .../ico/ico-bmp-4bpp/ico-size-31x31-4bpp.ico | Bin 0 -> 746 bytes .../ico/ico-bmp-4bpp/ico-size-31x31-4bpp.png | Bin 0 -> 700 bytes .../ico/ico-bmp-4bpp/ico-size-32x32-4bpp.ico | Bin 0 -> 766 bytes .../ico/ico-bmp-4bpp/ico-size-32x32-4bpp.png | Bin 0 -> 763 bytes .../ico/ico-bmp-4bpp/ico-size-33x33-4bpp.ico | Bin 0 -> 1050 bytes .../ico/ico-bmp-4bpp/ico-size-33x33-4bpp.png | Bin 0 -> 778 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.ico | Bin 0 -> 150 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.png | Bin 0 -> 139 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.ico | Bin 0 -> 158 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.png | Bin 0 -> 147 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.ico | Bin 0 -> 166 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.png | Bin 0 -> 156 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.ico | Bin 0 -> 174 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.png | Bin 0 -> 163 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.ico | Bin 0 -> 182 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.png | Bin 0 -> 172 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.ico | Bin 0 -> 190 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.png | Bin 0 -> 188 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.ico | Bin 0 -> 234 bytes .../reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.png | Bin 0 -> 198 bytes .../ico/ico-bmp-4bpp/ico-transparent-4bpp.ico | Bin 0 -> 3262 bytes .../ico/ico-bmp-4bpp/ico-transparent-4bpp.png | Bin 0 -> 195 bytes image/test/reftest/ico/ico-bmp-4bpp/reftest.list | 23 + .../ico-not-square-transparent-8bpp.ico | Bin 0 -> 1478 bytes .../ico-not-square-transparent-8bpp.png | Bin 0 -> 514 bytes .../ico-bmp-8bpp/ico-partial-transparent-8bpp.ico | Bin 0 -> 2238 bytes .../ico-bmp-8bpp/ico-partial-transparent-8bpp.png | Bin 0 -> 983 bytes .../ico/ico-bmp-8bpp/ico-size-15x15-8bpp.ico | Bin 0 -> 1386 bytes .../ico/ico-bmp-8bpp/ico-size-15x15-8bpp.png | Bin 0 -> 809 bytes .../ico/ico-bmp-8bpp/ico-size-16x16-8bpp.ico | Bin 0 -> 1406 bytes .../ico/ico-bmp-8bpp/ico-size-16x16-8bpp.png | Bin 0 -> 903 bytes .../ico/ico-bmp-8bpp/ico-size-17x17-8bpp.ico | Bin 0 -> 1494 bytes .../ico/ico-bmp-8bpp/ico-size-17x17-8bpp.png | Bin 0 -> 964 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.ico | Bin 0 -> 1094 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.png | Bin 0 -> 70 bytes .../ico/ico-bmp-8bpp/ico-size-256x256-8bpp.ico | Bin 0 -> 74814 bytes .../ico/ico-bmp-8bpp/ico-size-256x256-8bpp.png | Bin 0 -> 22443 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.ico | Bin 0 -> 1102 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.png | Bin 0 -> 83 bytes .../ico/ico-bmp-8bpp/ico-size-31x31-8bpp.ico | Bin 0 -> 2238 bytes .../ico/ico-bmp-8bpp/ico-size-31x31-8bpp.png | Bin 0 -> 1546 bytes .../ico/ico-bmp-8bpp/ico-size-32x32-8bpp.ico | Bin 0 -> 2238 bytes .../ico/ico-bmp-8bpp/ico-size-32x32-8bpp.png | Bin 0 -> 1530 bytes .../ico/ico-bmp-8bpp/ico-size-33x33-8bpp.ico | Bin 0 -> 2538 bytes .../ico/ico-bmp-8bpp/ico-size-33x33-8bpp.png | Bin 0 -> 1632 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.ico | Bin 0 -> 1110 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.png | Bin 0 -> 107 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.ico | Bin 0 -> 1118 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.png | Bin 0 -> 136 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.ico | Bin 0 -> 1146 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.png | Bin 0 -> 173 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.ico | Bin 0 -> 1158 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.png | Bin 0 -> 218 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.ico | Bin 0 -> 1170 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.png | Bin 0 -> 271 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.ico | Bin 0 -> 286 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.png | Bin 0 -> 313 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.ico | Bin 0 -> 1230 bytes .../reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.png | Bin 0 -> 368 bytes .../ico/ico-bmp-8bpp/ico-transparent-8bpp.ico | Bin 0 -> 3262 bytes .../ico/ico-bmp-8bpp/ico-transparent-8bpp.png | Bin 0 -> 195 bytes image/test/reftest/ico/ico-bmp-8bpp/reftest.list | 23 + image/test/reftest/ico/ico-bmp-corrupted/16x16.png | Bin 0 -> 879 bytes .../reftest/ico/ico-bmp-corrupted/invalid-bpp.ico | Bin 0 -> 86 bytes .../ico-bmp-corrupted/invalid-compression-RLE4.ico | Bin 0 -> 86 bytes .../ico-bmp-corrupted/invalid-compression-RLE8.ico | Bin 0 -> 86 bytes .../ico/ico-bmp-corrupted/invalid-compression.ico | Bin 0 -> 830 bytes .../reftest/ico/ico-bmp-corrupted/reftest.list | 10 + .../reftest/ico/ico-bmp-corrupted/wrapper.html | 80 + image/test/reftest/ico/ico-mixed/mixed-bmp-png.ico | Bin 0 -> 17542 bytes image/test/reftest/ico/ico-mixed/mixed-bmp-png.png | Bin 0 -> 629 bytes .../test/reftest/ico/ico-mixed/mixed-bmp-png32.png | Bin 0 -> 940 bytes .../test/reftest/ico/ico-mixed/mixed-bmp-png48.png | Bin 0 -> 1332 bytes image/test/reftest/ico/ico-mixed/reftest.list | 3 + .../reftest/ico/ico-png/corrupted_x00n0g01.ico | Bin 0 -> 71 bytes .../reftest/ico/ico-png/corrupted_xxcrn0g04.ico | Bin 0 -> 283 bytes .../reftest/ico/ico-png/ico-size-15x15-png.ico | Bin 0 -> 831 bytes .../reftest/ico/ico-png/ico-size-15x15-png.png | Bin 0 -> 809 bytes .../reftest/ico/ico-png/ico-size-16x16-png.ico | Bin 0 -> 901 bytes .../reftest/ico/ico-png/ico-size-16x16-png.png | Bin 0 -> 879 bytes .../reftest/ico/ico-png/ico-size-17x17-png.ico | Bin 0 -> 1022 bytes .../reftest/ico/ico-png/ico-size-17x17-png.png | Bin 0 -> 1000 bytes .../test/reftest/ico/ico-png/ico-size-1x1-png.ico | Bin 0 -> 92 bytes .../test/reftest/ico/ico-png/ico-size-1x1-png.png | Bin 0 -> 70 bytes .../reftest/ico/ico-png/ico-size-256x256-png.ico | Bin 0 -> 5934 bytes .../reftest/ico/ico-png/ico-size-256x256-png.png | Bin 0 -> 5912 bytes .../test/reftest/ico/ico-png/ico-size-2x2-png.ico | Bin 0 -> 105 bytes .../test/reftest/ico/ico-png/ico-size-2x2-png.png | Bin 0 -> 83 bytes .../reftest/ico/ico-png/ico-size-31x31-png.ico | Bin 0 -> 2958 bytes .../reftest/ico/ico-png/ico-size-31x31-png.png | Bin 0 -> 2936 bytes .../reftest/ico/ico-png/ico-size-32x32-png.ico | Bin 0 -> 3128 bytes .../reftest/ico/ico-png/ico-size-32x32-png.png | Bin 0 -> 3106 bytes .../reftest/ico/ico-png/ico-size-33x33-png.ico | Bin 0 -> 3325 bytes .../reftest/ico/ico-png/ico-size-33x33-png.png | Bin 0 -> 3303 bytes .../test/reftest/ico/ico-png/ico-size-3x3-png.ico | Bin 0 -> 129 bytes .../test/reftest/ico/ico-png/ico-size-3x3-png.png | Bin 0 -> 107 bytes .../test/reftest/ico/ico-png/ico-size-4x4-png.ico | Bin 0 -> 158 bytes .../test/reftest/ico/ico-png/ico-size-4x4-png.png | Bin 0 -> 136 bytes .../test/reftest/ico/ico-png/ico-size-5x5-png.ico | Bin 0 -> 195 bytes .../test/reftest/ico/ico-png/ico-size-5x5-png.png | Bin 0 -> 173 bytes .../test/reftest/ico/ico-png/ico-size-6x6-png.ico | Bin 0 -> 240 bytes .../test/reftest/ico/ico-png/ico-size-6x6-png.png | Bin 0 -> 218 bytes .../test/reftest/ico/ico-png/ico-size-7x7-png.ico | Bin 0 -> 293 bytes .../test/reftest/ico/ico-png/ico-size-7x7-png.png | Bin 0 -> 271 bytes .../test/reftest/ico/ico-png/ico-size-8x8-png.ico | Bin 0 -> 335 bytes .../test/reftest/ico/ico-png/ico-size-8x8-png.png | Bin 0 -> 313 bytes .../test/reftest/ico/ico-png/ico-size-9x9-png.ico | Bin 0 -> 390 bytes .../test/reftest/ico/ico-png/ico-size-9x9-png.png | Bin 0 -> 368 bytes image/test/reftest/ico/ico-png/reftest.list | 29 + image/test/reftest/ico/ico-png/tmp.ico | Bin 0 -> 1150 bytes image/test/reftest/ico/ico-png/transparent-png.ico | Bin 0 -> 1150 bytes image/test/reftest/ico/ico-png/transparent-png.png | Bin 0 -> 699 bytes image/test/reftest/ico/ico-png/wrapper.html | 28 + image/test/reftest/ico/ico-png/x00n0g01.png | Bin 0 -> 49 bytes image/test/reftest/ico/ico-png/xcrn0g04.png | Bin 0 -> 261 bytes image/test/reftest/ico/reftest.list | 11 + image/test/reftest/img2html.html | 122 + image/test/reftest/jpeg/blue.html | 1 + image/test/reftest/jpeg/blue.jpg | Bin 0 -> 3937 bytes image/test/reftest/jpeg/jpg-cmyk-1.jpg | Bin 0 -> 1498 bytes image/test/reftest/jpeg/jpg-cmyk-1.png | Bin 0 -> 2523 bytes image/test/reftest/jpeg/jpg-cmyk-2.jpg | Bin 0 -> 5174 bytes image/test/reftest/jpeg/jpg-cmyk-2.png | Bin 0 -> 21147 bytes image/test/reftest/jpeg/jpg-gray.jpg | Bin 0 -> 396 bytes image/test/reftest/jpeg/jpg-gray.png | Bin 0 -> 498 bytes .../reftest/jpeg/jpg-progressive-1000-ref.html | 1 + image/test/reftest/jpeg/jpg-progressive-1000.html | 1 + image/test/reftest/jpeg/jpg-progressive-1000.jpg | Bin 0 -> 34409 bytes image/test/reftest/jpeg/jpg-progressive.jpg | Bin 0 -> 979 bytes image/test/reftest/jpeg/jpg-progressive.png | Bin 0 -> 3106 bytes image/test/reftest/jpeg/jpg-size-15x15.jpg | Bin 0 -> 465 bytes image/test/reftest/jpeg/jpg-size-15x15.png | Bin 0 -> 809 bytes image/test/reftest/jpeg/jpg-size-16x16.jpg | Bin 0 -> 443 bytes image/test/reftest/jpeg/jpg-size-16x16.png | Bin 0 -> 879 bytes image/test/reftest/jpeg/jpg-size-17x17.jpg | Bin 0 -> 582 bytes image/test/reftest/jpeg/jpg-size-17x17.png | Bin 0 -> 1000 bytes image/test/reftest/jpeg/jpg-size-1x1.jpg | Bin 0 -> 288 bytes image/test/reftest/jpeg/jpg-size-1x1.png | Bin 0 -> 70 bytes image/test/reftest/jpeg/jpg-size-2x2.jpg | Bin 0 -> 353 bytes image/test/reftest/jpeg/jpg-size-2x2.png | Bin 0 -> 83 bytes image/test/reftest/jpeg/jpg-size-31x31.jpg | Bin 0 -> 773 bytes image/test/reftest/jpeg/jpg-size-31x31.png | Bin 0 -> 2936 bytes image/test/reftest/jpeg/jpg-size-32x32.jpg | Bin 0 -> 759 bytes image/test/reftest/jpeg/jpg-size-32x32.png | Bin 0 -> 3106 bytes image/test/reftest/jpeg/jpg-size-33x33.jpg | Bin 0 -> 941 bytes image/test/reftest/jpeg/jpg-size-33x33.png | Bin 0 -> 3303 bytes image/test/reftest/jpeg/jpg-size-3x3.jpg | Bin 0 -> 429 bytes image/test/reftest/jpeg/jpg-size-3x3.png | Bin 0 -> 107 bytes image/test/reftest/jpeg/jpg-size-4x4.jpg | Bin 0 -> 427 bytes image/test/reftest/jpeg/jpg-size-4x4.png | Bin 0 -> 136 bytes image/test/reftest/jpeg/jpg-size-5x5.jpg | Bin 0 -> 421 bytes image/test/reftest/jpeg/jpg-size-5x5.png | Bin 0 -> 173 bytes image/test/reftest/jpeg/jpg-size-6x6.jpg | Bin 0 -> 218 bytes image/test/reftest/jpeg/jpg-size-6x6.png | Bin 0 -> 218 bytes image/test/reftest/jpeg/jpg-size-7x7.jpg | Bin 0 -> 420 bytes image/test/reftest/jpeg/jpg-size-7x7.png | Bin 0 -> 271 bytes image/test/reftest/jpeg/jpg-size-8x8.jpg | Bin 0 -> 412 bytes image/test/reftest/jpeg/jpg-size-8x8.png | Bin 0 -> 313 bytes image/test/reftest/jpeg/jpg-size-9x9.jpg | Bin 0 -> 438 bytes image/test/reftest/jpeg/jpg-size-9x9.png | Bin 0 -> 368 bytes image/test/reftest/jpeg/jpg-srgb-icc.jpg | Bin 0 -> 3226 bytes image/test/reftest/jpeg/jpg-srgb-icc.png | Bin 0 -> 2738 bytes .../non-interleaved_progressive-1-halfred-ref.png | Bin 0 -> 3486 bytes .../reftest/jpeg/non-interleaved_progressive-1.jpg | Bin 0 -> 1777 bytes .../non-interleaved_progressive-2-white-ref.png | Bin 0 -> 2303 bytes .../reftest/jpeg/non-interleaved_progressive-2.jpg | Bin 0 -> 3000 bytes image/test/reftest/jpeg/red-bad-marker.jpg | Bin 0 -> 640 bytes image/test/reftest/jpeg/red.jpg | Bin 0 -> 3938 bytes image/test/reftest/jpeg/reftest.list | 73 + image/test/reftest/jpeg/webcam-simulacrum.html | 1 + image/test/reftest/jpeg/webcam-simulacrum.mjpg | Bin 0 -> 7978 bytes .../reftest/jpeg/webcam-simulacrum.mjpg^headers^ | 3 + image/test/reftest/jxl/jxl-size-33x33.jxl | Bin 0 -> 916 bytes image/test/reftest/jxl/jxl-size-33x33.png | Bin 0 -> 3303 bytes image/test/reftest/jxl/reftest.list | 3 + .../test/reftest/pngsuite-ancillary/ccwn2c08.html | 1242 ++++++++ image/test/reftest/pngsuite-ancillary/ccwn2c08.png | Bin 0 -> 1514 bytes .../test/reftest/pngsuite-ancillary/ccwn3p08.html | 1272 ++++++++ image/test/reftest/pngsuite-ancillary/ccwn3p08.png | Bin 0 -> 1554 bytes .../test/reftest/pngsuite-ancillary/cdfn2c08.html | 326 ++ image/test/reftest/pngsuite-ancillary/cdfn2c08.png | Bin 0 -> 404 bytes .../test/reftest/pngsuite-ancillary/cdhn2c08.html | 278 ++ image/test/reftest/pngsuite-ancillary/cdhn2c08.png | Bin 0 -> 344 bytes .../test/reftest/pngsuite-ancillary/cdsn2c08.html | 86 + image/test/reftest/pngsuite-ancillary/cdsn2c08.png | Bin 0 -> 232 bytes .../test/reftest/pngsuite-ancillary/cdun2c08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cdun2c08.png | Bin 0 -> 724 bytes .../test/reftest/pngsuite-ancillary/ch1n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/ch1n3p04.png | Bin 0 -> 258 bytes .../test/reftest/pngsuite-ancillary/ch2n3p08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/ch2n3p08.png | Bin 0 -> 1810 bytes .../test/reftest/pngsuite-ancillary/cm0n0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cm0n0g04.png | Bin 0 -> 292 bytes .../test/reftest/pngsuite-ancillary/cm7n0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cm7n0g04.png | Bin 0 -> 292 bytes .../test/reftest/pngsuite-ancillary/cm9n0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cm9n0g04.png | Bin 0 -> 292 bytes .../test/reftest/pngsuite-ancillary/cs3n2c16.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs3n2c16.png | Bin 0 -> 214 bytes .../test/reftest/pngsuite-ancillary/cs3n3p08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs3n3p08.png | Bin 0 -> 259 bytes .../test/reftest/pngsuite-ancillary/cs5n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs5n2c08.png | Bin 0 -> 186 bytes .../test/reftest/pngsuite-ancillary/cs5n3p08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs5n3p08.png | Bin 0 -> 271 bytes .../test/reftest/pngsuite-ancillary/cs8n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs8n2c08.png | Bin 0 -> 149 bytes .../test/reftest/pngsuite-ancillary/cs8n3p08.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/cs8n3p08.png | Bin 0 -> 256 bytes .../test/reftest/pngsuite-ancillary/ct0n0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/ct0n0g04.png | Bin 0 -> 273 bytes .../test/reftest/pngsuite-ancillary/ct1n0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/ct1n0g04.png | Bin 0 -> 792 bytes .../test/reftest/pngsuite-ancillary/ctzn0g04.html | 1094 +++++++ image/test/reftest/pngsuite-ancillary/ctzn0g04.png | Bin 0 -> 753 bytes .../reftest/pngsuite-ancillary/qcms-asm-check.js | 28 + image/test/reftest/pngsuite-ancillary/reftest.list | 62 + .../test/reftest/pngsuite-background/bg__4a08.html | 1092 +++++++ .../test/reftest/pngsuite-background/bg__4a16.html | 1092 +++++++ .../test/reftest/pngsuite-background/bg__6a08.html | 1092 +++++++ .../test/reftest/pngsuite-background/bg__6a16.html | 1092 +++++++ .../test/reftest/pngsuite-background/bgai4a08.png | Bin 0 -> 214 bytes .../test/reftest/pngsuite-background/bgai4a16.png | Bin 0 -> 2855 bytes .../test/reftest/pngsuite-background/bgan6a08.png | Bin 0 -> 184 bytes .../test/reftest/pngsuite-background/bgan6a16.png | Bin 0 -> 3435 bytes .../test/reftest/pngsuite-background/bgbn4a08.png | Bin 0 -> 140 bytes .../test/reftest/pngsuite-background/bggn4a16.png | Bin 0 -> 2220 bytes .../test/reftest/pngsuite-background/bgwn6a08.png | Bin 0 -> 202 bytes .../test/reftest/pngsuite-background/bgyn6a16.png | Bin 0 -> 3453 bytes .../test/reftest/pngsuite-background/reftest.list | 22 + .../test/reftest/pngsuite-background/wrapper.html | 28 + image/test/reftest/pngsuite-basic-i/basi0g01.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi0g01.png | Bin 0 -> 217 bytes image/test/reftest/pngsuite-basic-i/basi0g02.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi0g02.png | Bin 0 -> 154 bytes image/test/reftest/pngsuite-basic-i/basi0g04.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi0g04.png | Bin 0 -> 247 bytes image/test/reftest/pngsuite-basic-i/basi0g08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi0g08.png | Bin 0 -> 254 bytes image/test/reftest/pngsuite-basic-i/basi0g16.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi0g16.png | Bin 0 -> 299 bytes image/test/reftest/pngsuite-basic-i/basi2c08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi2c08.png | Bin 0 -> 315 bytes image/test/reftest/pngsuite-basic-i/basi2c16.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi2c16.png | Bin 0 -> 595 bytes image/test/reftest/pngsuite-basic-i/basi3p01.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi3p01.png | Bin 0 -> 132 bytes image/test/reftest/pngsuite-basic-i/basi3p02.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi3p02.png | Bin 0 -> 193 bytes image/test/reftest/pngsuite-basic-i/basi3p04.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi3p04.png | Bin 0 -> 327 bytes image/test/reftest/pngsuite-basic-i/basi3p08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-i/basi3p08.png | Bin 0 -> 1527 bytes image/test/reftest/pngsuite-basic-i/basi4a08.png | Bin 0 -> 214 bytes image/test/reftest/pngsuite-basic-i/basi4a16.png | Bin 0 -> 2855 bytes image/test/reftest/pngsuite-basic-i/basi6a08.png | Bin 0 -> 361 bytes image/test/reftest/pngsuite-basic-i/basi6a16.png | Bin 0 -> 4180 bytes image/test/reftest/pngsuite-basic-i/reftest.list | 33 + image/test/reftest/pngsuite-basic-n/basn0g01.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn0g01.png | Bin 0 -> 164 bytes image/test/reftest/pngsuite-basic-n/basn0g02.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn0g02.png | Bin 0 -> 104 bytes image/test/reftest/pngsuite-basic-n/basn0g04.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn0g04.png | Bin 0 -> 145 bytes image/test/reftest/pngsuite-basic-n/basn0g08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn0g08.png | Bin 0 -> 138 bytes image/test/reftest/pngsuite-basic-n/basn0g16.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn0g16.png | Bin 0 -> 167 bytes image/test/reftest/pngsuite-basic-n/basn2c08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn2c08.png | Bin 0 -> 145 bytes image/test/reftest/pngsuite-basic-n/basn2c16.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn2c16.png | Bin 0 -> 302 bytes image/test/reftest/pngsuite-basic-n/basn3p01.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn3p01.png | Bin 0 -> 112 bytes image/test/reftest/pngsuite-basic-n/basn3p02.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn3p02.png | Bin 0 -> 146 bytes image/test/reftest/pngsuite-basic-n/basn3p04.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn3p04.png | Bin 0 -> 216 bytes image/test/reftest/pngsuite-basic-n/basn3p08.html | 1094 +++++++ image/test/reftest/pngsuite-basic-n/basn3p08.png | Bin 0 -> 1286 bytes image/test/reftest/pngsuite-basic-n/basn4a08.png | Bin 0 -> 126 bytes image/test/reftest/pngsuite-basic-n/basn4a16.png | Bin 0 -> 2206 bytes image/test/reftest/pngsuite-basic-n/basn6a08.png | Bin 0 -> 184 bytes image/test/reftest/pngsuite-basic-n/basn6a16.png | Bin 0 -> 3435 bytes image/test/reftest/pngsuite-basic-n/reftest.list | 33 + image/test/reftest/pngsuite-chunkorder/color.html | 1094 +++++++ .../reftest/pngsuite-chunkorder/grayscale.html | 1094 +++++++ .../test/reftest/pngsuite-chunkorder/oi1n0g16.png | Bin 0 -> 167 bytes .../test/reftest/pngsuite-chunkorder/oi1n2c16.png | Bin 0 -> 302 bytes .../test/reftest/pngsuite-chunkorder/oi2n0g16.png | Bin 0 -> 179 bytes .../test/reftest/pngsuite-chunkorder/oi2n2c16.png | Bin 0 -> 314 bytes .../test/reftest/pngsuite-chunkorder/oi4n0g16.png | Bin 0 -> 203 bytes .../test/reftest/pngsuite-chunkorder/oi4n2c16.png | Bin 0 -> 338 bytes .../test/reftest/pngsuite-chunkorder/oi9n0g16.png | Bin 0 -> 1283 bytes .../test/reftest/pngsuite-chunkorder/oi9n2c16.png | Bin 0 -> 3038 bytes .../test/reftest/pngsuite-chunkorder/reftest.list | 21 + image/test/reftest/pngsuite-corrupted/reftest.list | 10 + image/test/reftest/pngsuite-corrupted/wrapper.html | 28 + image/test/reftest/pngsuite-corrupted/x00n0g01.png | Bin 0 -> 49 bytes image/test/reftest/pngsuite-corrupted/xcrn0g04.png | Bin 0 -> 261 bytes image/test/reftest/pngsuite-corrupted/xlfn0g04.png | 13 + .../test/reftest/pngsuite-filtering/f00n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f00n0g08.png | Bin 0 -> 319 bytes .../test/reftest/pngsuite-filtering/f00n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f00n2c08.png | Bin 0 -> 2475 bytes .../test/reftest/pngsuite-filtering/f01n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f01n0g08.png | Bin 0 -> 321 bytes .../test/reftest/pngsuite-filtering/f01n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f01n2c08.png | Bin 0 -> 1180 bytes .../test/reftest/pngsuite-filtering/f02n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f02n0g08.png | Bin 0 -> 355 bytes .../test/reftest/pngsuite-filtering/f02n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f02n2c08.png | Bin 0 -> 1729 bytes .../test/reftest/pngsuite-filtering/f03n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f03n0g08.png | Bin 0 -> 389 bytes .../test/reftest/pngsuite-filtering/f03n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f03n2c08.png | Bin 0 -> 1291 bytes .../test/reftest/pngsuite-filtering/f04n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f04n0g08.png | Bin 0 -> 269 bytes .../test/reftest/pngsuite-filtering/f04n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-filtering/f04n2c08.png | Bin 0 -> 985 bytes image/test/reftest/pngsuite-filtering/reftest.list | 22 + image/test/reftest/pngsuite-gamma/g03n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g03n0g16.png | Bin 0 -> 345 bytes image/test/reftest/pngsuite-gamma/g03n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g03n2c08.png | Bin 0 -> 370 bytes image/test/reftest/pngsuite-gamma/g03n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g03n3p04.png | Bin 0 -> 214 bytes image/test/reftest/pngsuite-gamma/g04n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g04n0g16.png | Bin 0 -> 363 bytes image/test/reftest/pngsuite-gamma/g04n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g04n2c08.png | Bin 0 -> 377 bytes image/test/reftest/pngsuite-gamma/g04n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g04n3p04.png | Bin 0 -> 219 bytes image/test/reftest/pngsuite-gamma/g05n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g05n0g16.png | Bin 0 -> 339 bytes image/test/reftest/pngsuite-gamma/g05n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g05n2c08.png | Bin 0 -> 350 bytes image/test/reftest/pngsuite-gamma/g05n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g05n3p04.png | Bin 0 -> 206 bytes image/test/reftest/pngsuite-gamma/g07n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g07n0g16.png | Bin 0 -> 321 bytes image/test/reftest/pngsuite-gamma/g07n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g07n2c08.png | Bin 0 -> 340 bytes image/test/reftest/pngsuite-gamma/g07n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g07n3p04.png | Bin 0 -> 207 bytes image/test/reftest/pngsuite-gamma/g10n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g10n0g16.png | Bin 0 -> 262 bytes image/test/reftest/pngsuite-gamma/g10n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g10n2c08.png | Bin 0 -> 285 bytes image/test/reftest/pngsuite-gamma/g10n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g10n3p04.png | Bin 0 -> 214 bytes image/test/reftest/pngsuite-gamma/g25n0g16.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g25n0g16.png | Bin 0 -> 383 bytes image/test/reftest/pngsuite-gamma/g25n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g25n2c08.png | Bin 0 -> 405 bytes image/test/reftest/pngsuite-gamma/g25n3p04.html | 1094 +++++++ image/test/reftest/pngsuite-gamma/g25n3p04.png | Bin 0 -> 215 bytes image/test/reftest/pngsuite-gamma/reftest.list | 38 + image/test/reftest/pngsuite-oddsizes/reftest.list | 77 + image/test/reftest/pngsuite-oddsizes/s01_3p01.html | 9 + image/test/reftest/pngsuite-oddsizes/s01i3p01.png | Bin 0 -> 113 bytes image/test/reftest/pngsuite-oddsizes/s01n3p01.png | Bin 0 -> 113 bytes image/test/reftest/pngsuite-oddsizes/s02_3p01.html | 14 + image/test/reftest/pngsuite-oddsizes/s02i3p01.png | Bin 0 -> 114 bytes image/test/reftest/pngsuite-oddsizes/s02n3p01.png | Bin 0 -> 115 bytes image/test/reftest/pngsuite-oddsizes/s03_3p01.html | 21 + image/test/reftest/pngsuite-oddsizes/s03i3p01.png | Bin 0 -> 118 bytes image/test/reftest/pngsuite-oddsizes/s03n3p01.png | Bin 0 -> 120 bytes image/test/reftest/pngsuite-oddsizes/s04_3p01.html | 30 + image/test/reftest/pngsuite-oddsizes/s04i3p01.png | Bin 0 -> 126 bytes image/test/reftest/pngsuite-oddsizes/s04n3p01.png | Bin 0 -> 121 bytes image/test/reftest/pngsuite-oddsizes/s05_3p02.html | 41 + image/test/reftest/pngsuite-oddsizes/s05i3p02.png | Bin 0 -> 134 bytes image/test/reftest/pngsuite-oddsizes/s05n3p02.png | Bin 0 -> 129 bytes image/test/reftest/pngsuite-oddsizes/s06_3p02.html | 54 + image/test/reftest/pngsuite-oddsizes/s06i3p02.png | Bin 0 -> 143 bytes image/test/reftest/pngsuite-oddsizes/s06n3p02.png | Bin 0 -> 131 bytes image/test/reftest/pngsuite-oddsizes/s07_3p02.html | 69 + image/test/reftest/pngsuite-oddsizes/s07i3p02.png | Bin 0 -> 149 bytes image/test/reftest/pngsuite-oddsizes/s07n3p02.png | Bin 0 -> 138 bytes image/test/reftest/pngsuite-oddsizes/s08_3p02.html | 86 + image/test/reftest/pngsuite-oddsizes/s08i3p02.png | Bin 0 -> 149 bytes image/test/reftest/pngsuite-oddsizes/s08n3p02.png | Bin 0 -> 139 bytes image/test/reftest/pngsuite-oddsizes/s09_3p02.html | 105 + image/test/reftest/pngsuite-oddsizes/s09i3p02.png | Bin 0 -> 147 bytes image/test/reftest/pngsuite-oddsizes/s09n3p02.png | Bin 0 -> 143 bytes image/test/reftest/pngsuite-oddsizes/s32_3p04.html | 1094 +++++++ image/test/reftest/pngsuite-oddsizes/s32i3p04.png | Bin 0 -> 355 bytes image/test/reftest/pngsuite-oddsizes/s32n3p04.png | Bin 0 -> 263 bytes image/test/reftest/pngsuite-oddsizes/s33_3p04.html | 1161 +++++++ image/test/reftest/pngsuite-oddsizes/s33i3p04.png | Bin 0 -> 385 bytes image/test/reftest/pngsuite-oddsizes/s33n3p04.png | Bin 0 -> 329 bytes image/test/reftest/pngsuite-oddsizes/s34_3p04.html | 1230 ++++++++ image/test/reftest/pngsuite-oddsizes/s34i3p04.png | Bin 0 -> 349 bytes image/test/reftest/pngsuite-oddsizes/s34n3p04.png | Bin 0 -> 248 bytes image/test/reftest/pngsuite-oddsizes/s35_3p04.html | 1301 ++++++++ image/test/reftest/pngsuite-oddsizes/s35i3p04.png | Bin 0 -> 399 bytes image/test/reftest/pngsuite-oddsizes/s35n3p04.png | Bin 0 -> 338 bytes image/test/reftest/pngsuite-oddsizes/s36_3p04.html | 1374 ++++++++ image/test/reftest/pngsuite-oddsizes/s36i3p04.png | Bin 0 -> 356 bytes image/test/reftest/pngsuite-oddsizes/s36n3p04.png | Bin 0 -> 258 bytes image/test/reftest/pngsuite-oddsizes/s37_3p04.html | 1449 +++++++++ image/test/reftest/pngsuite-oddsizes/s37i3p04.png | Bin 0 -> 393 bytes image/test/reftest/pngsuite-oddsizes/s37n3p04.png | Bin 0 -> 336 bytes image/test/reftest/pngsuite-oddsizes/s38_3p04.html | 1526 +++++++++ image/test/reftest/pngsuite-oddsizes/s38i3p04.png | Bin 0 -> 357 bytes image/test/reftest/pngsuite-oddsizes/s38n3p04.png | Bin 0 -> 245 bytes image/test/reftest/pngsuite-oddsizes/s39_3p04.html | 1605 ++++++++++ image/test/reftest/pngsuite-oddsizes/s39i3p04.png | Bin 0 -> 420 bytes image/test/reftest/pngsuite-oddsizes/s39n3p04.png | Bin 0 -> 352 bytes image/test/reftest/pngsuite-oddsizes/s40_3p04.html | 1686 ++++++++++ image/test/reftest/pngsuite-oddsizes/s40i3p04.png | Bin 0 -> 357 bytes image/test/reftest/pngsuite-oddsizes/s40n3p04.png | Bin 0 -> 256 bytes image/test/reftest/pngsuite-palettes/pp0n2c16.html | 1094 +++++++ image/test/reftest/pngsuite-palettes/pp0n2c16.png | Bin 0 -> 962 bytes image/test/reftest/pngsuite-palettes/pp0n6a08.png | Bin 0 -> 818 bytes image/test/reftest/pngsuite-palettes/ps1n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-palettes/ps1n0g08.png | Bin 0 -> 1477 bytes image/test/reftest/pngsuite-palettes/ps1n2c16.html | 1094 +++++++ image/test/reftest/pngsuite-palettes/ps1n2c16.png | Bin 0 -> 1641 bytes image/test/reftest/pngsuite-palettes/ps2n0g08.html | 1094 +++++++ image/test/reftest/pngsuite-palettes/ps2n0g08.png | Bin 0 -> 2341 bytes image/test/reftest/pngsuite-palettes/ps2n2c16.html | 1094 +++++++ image/test/reftest/pngsuite-palettes/ps2n2c16.png | Bin 0 -> 2505 bytes image/test/reftest/pngsuite-palettes/reftest.list | 14 + image/test/reftest/pngsuite-zlib/reftest.list | 8 + image/test/reftest/pngsuite-zlib/z00n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-zlib/z00n2c08.png | Bin 0 -> 3172 bytes image/test/reftest/pngsuite-zlib/z03n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-zlib/z03n2c08.png | Bin 0 -> 232 bytes image/test/reftest/pngsuite-zlib/z06n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-zlib/z06n2c08.png | Bin 0 -> 224 bytes image/test/reftest/pngsuite-zlib/z09n2c08.html | 1094 +++++++ image/test/reftest/pngsuite-zlib/z09n2c08.png | Bin 0 -> 224 bytes image/test/reftest/reftest.list | 55 + image/test/reftest/webp/blue.png | Bin 0 -> 100 bytes image/test/reftest/webp/icc-bit-no-icc-chunk.webp | Bin 0 -> 78 bytes image/test/reftest/webp/reftest.list | 1 + image/test/unit/async_load_tests.js | 298 ++ image/test/unit/bug413512.ico | Bin 0 -> 17759 bytes image/test/unit/bug815359.ico | Bin 0 -> 4286 bytes image/test/unit/image1.png | Bin 0 -> 8415 bytes image/test/unit/image1.webp | Bin 0 -> 3206 bytes image/test/unit/image1png16x16.jpg | Bin 0 -> 1050 bytes image/test/unit/image1png64x64.jpg | Bin 0 -> 4507 bytes image/test/unit/image1quality50.webp | Bin 0 -> 1944 bytes image/test/unit/image2.jpg | Bin 0 -> 3494 bytes image/test/unit/image2jpg16x16-win.png | Bin 0 -> 948 bytes image/test/unit/image2jpg16x16.png | Bin 0 -> 955 bytes image/test/unit/image2jpg16x16cropped.jpg | Bin 0 -> 879 bytes image/test/unit/image2jpg16x16cropped2.jpg | Bin 0 -> 878 bytes image/test/unit/image2jpg16x32cropped3.jpg | Bin 0 -> 1127 bytes image/test/unit/image2jpg16x32scaled.jpg | Bin 0 -> 1219 bytes image/test/unit/image2jpg32x16cropped4.jpg | Bin 0 -> 1135 bytes image/test/unit/image2jpg32x16scaled.jpg | Bin 0 -> 1227 bytes image/test/unit/image2jpg32x32-win.png | Bin 0 -> 3104 bytes image/test/unit/image2jpg32x32.jpg | Bin 0 -> 1634 bytes image/test/unit/image2jpg32x32.png | Bin 0 -> 3026 bytes image/test/unit/image3.ico | Bin 0 -> 1406 bytes image/test/unit/image3ico16x16.png | Bin 0 -> 520 bytes image/test/unit/image3ico32x32.png | Bin 0 -> 2280 bytes image/test/unit/image4.gif | Bin 0 -> 1809 bytes image/test/unit/image4gif16x16bmp24bpp.ico | Bin 0 -> 894 bytes image/test/unit/image4gif16x16bmp32bpp.ico | Bin 0 -> 1150 bytes image/test/unit/image4gif32x32bmp24bpp.ico | Bin 0 -> 3262 bytes image/test/unit/image4gif32x32bmp32bpp.ico | Bin 0 -> 4286 bytes image/test/unit/image_load_helpers.js | 124 + image/test/unit/test_async_notification.js | 15 + image/test/unit/test_async_notification_404.js | 23 + .../test/unit/test_async_notification_animated.js | 19 + image/test/unit/test_encoder_apng.js | 582 ++++ image/test/unit/test_encoder_png.js | 263 ++ image/test/unit/test_imgtools.js | 869 +++++ image/test/unit/test_moz_icon_uri.js | 157 + image/test/unit/test_private_channel.js | 166 + image/test/unit/xpcshell.ini | 42 + 1653 files changed, 184261 insertions(+) create mode 100644 image/AnimationFrameBuffer.cpp create mode 100644 image/AnimationFrameBuffer.h create mode 100644 image/AnimationParams.h create mode 100644 image/AnimationSurfaceProvider.cpp create mode 100644 image/AnimationSurfaceProvider.h create mode 100644 image/AutoRestoreSVGState.h create mode 100644 image/BMPHeaders.h create mode 100644 image/BlobSurfaceProvider.cpp create mode 100644 image/BlobSurfaceProvider.h create mode 100644 image/ClippedImage.cpp create mode 100644 image/ClippedImage.h create mode 100644 image/CopyOnWrite.h create mode 100644 image/DecodePool.cpp create mode 100644 image/DecodePool.h create mode 100644 image/DecodedSurfaceProvider.cpp create mode 100644 image/DecodedSurfaceProvider.h create mode 100644 image/Decoder.cpp create mode 100644 image/Decoder.h create mode 100644 image/DecoderFactory.cpp create mode 100644 image/DecoderFactory.h create mode 100644 image/DecoderFlags.h create mode 100644 image/Downscaler.cpp create mode 100644 image/Downscaler.h create mode 100644 image/DownscalingFilter.h create mode 100644 image/DynamicImage.cpp create mode 100644 image/DynamicImage.h create mode 100644 image/FrameAnimator.cpp create mode 100644 image/FrameAnimator.h create mode 100644 image/FrameTimeout.h create mode 100644 image/FrozenImage.cpp create mode 100644 image/FrozenImage.h create mode 100644 image/ICOFileHeaders.h create mode 100644 image/IDecodingTask.cpp create mode 100644 image/IDecodingTask.h create mode 100644 image/IProgressObserver.h create mode 100644 image/ISurfaceProvider.h create mode 100644 image/Image.cpp create mode 100644 image/Image.h create mode 100644 image/ImageBlocker.cpp create mode 100644 image/ImageBlocker.h create mode 100644 image/ImageCacheKey.cpp create mode 100644 image/ImageCacheKey.h create mode 100644 image/ImageFactory.cpp create mode 100644 image/ImageFactory.h create mode 100644 image/ImageLogging.h create mode 100644 image/ImageMemoryReporter.cpp create mode 100644 image/ImageMemoryReporter.h create mode 100644 image/ImageMetadata.h create mode 100644 image/ImageOps.cpp create mode 100644 image/ImageOps.h create mode 100644 image/ImageRegion.h create mode 100644 image/ImageWrapper.cpp create mode 100644 image/ImageWrapper.h create mode 100644 image/ImgDrawResult.h create mode 100644 image/LookupResult.h create mode 100644 image/MultipartImage.cpp create mode 100644 image/MultipartImage.h create mode 100644 image/Orientation.h create mode 100644 image/OrientedImage.cpp create mode 100644 image/OrientedImage.h create mode 100644 image/PlaybackType.h create mode 100644 image/ProgressTracker.cpp create mode 100644 image/ProgressTracker.h create mode 100644 image/RasterImage.cpp create mode 100644 image/RasterImage.h create mode 100644 image/Resolution.h create mode 100644 image/SVGDocumentWrapper.cpp create mode 100644 image/SVGDocumentWrapper.h create mode 100644 image/SVGDrawingCallback.h create mode 100644 image/SVGDrawingParameters.h create mode 100644 image/ScriptedNotificationObserver.cpp create mode 100644 image/ScriptedNotificationObserver.h create mode 100644 image/ShutdownTracker.cpp create mode 100644 image/ShutdownTracker.h create mode 100644 image/SourceBuffer.cpp create mode 100644 image/SourceBuffer.h create mode 100644 image/StreamingLexer.h create mode 100644 image/SurfaceCache.cpp create mode 100644 image/SurfaceCache.h create mode 100644 image/SurfaceCacheUtils.cpp create mode 100644 image/SurfaceCacheUtils.h create mode 100644 image/SurfaceFilters.h create mode 100644 image/SurfaceFlags.h create mode 100644 image/SurfacePipe.cpp create mode 100644 image/SurfacePipe.h create mode 100644 image/SurfacePipeFactory.h create mode 100644 image/VectorImage.cpp create mode 100644 image/VectorImage.h create mode 100644 image/WebRenderImageProvider.h create mode 100644 image/build/components.conf create mode 100644 image/build/moz.build create mode 100644 image/build/nsImageModule.cpp create mode 100644 image/build/nsImageModule.h create mode 100644 image/decoders/EXIF.cpp create mode 100644 image/decoders/EXIF.h create mode 100644 image/decoders/GIF2.h create mode 100644 image/decoders/iccjpeg.c create mode 100644 image/decoders/iccjpeg.h create mode 100644 image/decoders/icon/android/moz.build create mode 100644 image/decoders/icon/android/nsIconChannel.cpp create mode 100644 image/decoders/icon/android/nsIconChannel.h create mode 100644 image/decoders/icon/components.conf create mode 100644 image/decoders/icon/gtk/moz.build create mode 100644 image/decoders/icon/gtk/nsIconChannel.cpp create mode 100644 image/decoders/icon/gtk/nsIconChannel.h create mode 100644 image/decoders/icon/mac/moz.build create mode 100644 image/decoders/icon/mac/nsIconChannel.h create mode 100644 image/decoders/icon/mac/nsIconChannelCocoa.mm create mode 100644 image/decoders/icon/moz.build create mode 100644 image/decoders/icon/nsIconProtocolHandler.cpp create mode 100644 image/decoders/icon/nsIconProtocolHandler.h create mode 100644 image/decoders/icon/nsIconURI.cpp create mode 100644 image/decoders/icon/nsIconURI.h create mode 100644 image/decoders/icon/win/moz.build create mode 100644 image/decoders/icon/win/nsIconChannel.cpp create mode 100644 image/decoders/icon/win/nsIconChannel.h create mode 100644 image/decoders/moz.build create mode 100644 image/decoders/nsAVIFDecoder.cpp create mode 100644 image/decoders/nsAVIFDecoder.h create mode 100644 image/decoders/nsBMPDecoder.cpp create mode 100644 image/decoders/nsBMPDecoder.h create mode 100644 image/decoders/nsGIFDecoder2.cpp create mode 100644 image/decoders/nsGIFDecoder2.h create mode 100644 image/decoders/nsICODecoder.cpp create mode 100644 image/decoders/nsICODecoder.h create mode 100644 image/decoders/nsIconDecoder.cpp create mode 100644 image/decoders/nsIconDecoder.h create mode 100644 image/decoders/nsJPEGDecoder.cpp create mode 100644 image/decoders/nsJPEGDecoder.h create mode 100644 image/decoders/nsJXLDecoder.cpp create mode 100644 image/decoders/nsJXLDecoder.h create mode 100644 image/decoders/nsPNGDecoder.cpp create mode 100644 image/decoders/nsPNGDecoder.h create mode 100644 image/decoders/nsWebPDecoder.cpp create mode 100644 image/decoders/nsWebPDecoder.h create mode 100644 image/encoders/bmp/moz.build create mode 100644 image/encoders/bmp/nsBMPEncoder.cpp create mode 100644 image/encoders/bmp/nsBMPEncoder.h create mode 100644 image/encoders/ico/moz.build create mode 100644 image/encoders/ico/nsICOEncoder.cpp create mode 100644 image/encoders/ico/nsICOEncoder.h create mode 100644 image/encoders/jpeg/moz.build create mode 100644 image/encoders/jpeg/nsJPEGEncoder.cpp create mode 100644 image/encoders/jpeg/nsJPEGEncoder.h create mode 100644 image/encoders/moz.build create mode 100644 image/encoders/png/moz.build create mode 100644 image/encoders/png/nsPNGEncoder.cpp create mode 100644 image/encoders/png/nsPNGEncoder.h create mode 100644 image/encoders/webp/moz.build create mode 100644 image/encoders/webp/nsWebPEncoder.cpp create mode 100644 image/encoders/webp/nsWebPEncoder.h create mode 100644 image/imgFrame.cpp create mode 100644 image/imgFrame.h create mode 100644 image/imgICache.idl create mode 100644 image/imgIContainer.idl create mode 100644 image/imgIContainerDebug.idl create mode 100644 image/imgIEncoder.idl create mode 100644 image/imgILoader.idl create mode 100644 image/imgINotificationObserver.idl create mode 100644 image/imgIRequest.idl create mode 100644 image/imgIScriptedNotificationObserver.idl create mode 100644 image/imgITools.idl create mode 100644 image/imgLoader.cpp create mode 100644 image/imgLoader.h create mode 100644 image/imgRequest.cpp create mode 100644 image/imgRequest.h create mode 100644 image/imgRequestProxy.cpp create mode 100644 image/imgRequestProxy.h create mode 100644 image/imgTools.cpp create mode 100644 image/imgTools.h create mode 100644 image/moz.build create mode 100644 image/nsIIconURI.idl create mode 100644 image/test/browser/animated.gif create mode 100644 image/test/browser/animated2.gif create mode 100644 image/test/browser/big.png create mode 100644 image/test/browser/browser.ini create mode 100644 image/test/browser/browser_bug666317.js create mode 100644 image/test/browser/browser_docshell_type_editor.js create mode 100644 image/test/browser/browser_docshell_type_editor/chrome.manifest create mode 100644 image/test/browser/browser_docshell_type_editor/img/privileged.png create mode 100644 image/test/browser/browser_image.js create mode 100644 image/test/browser/browser_mozicon_file.js create mode 100644 image/test/browser/browser_mozicon_file_sandbox_headless.js create mode 100644 image/test/browser/browser_offscreen_image_in_out_of_process_iframe.js create mode 100644 image/test/browser/browser_sandbox_headless.ini create mode 100644 image/test/browser/empty.html create mode 100644 image/test/browser/head.js create mode 100644 image/test/browser/image.html create mode 100644 image/test/browser/imageX2.html create mode 100644 image/test/crashtests/1205923-1.html create mode 100644 image/test/crashtests/1210745-1.gif create mode 100644 image/test/crashtests/1212954-1.svg create mode 100644 image/test/crashtests/1235605.gif create mode 100644 image/test/crashtests/1241728-1.html create mode 100644 image/test/crashtests/1241729-1.bmp create mode 100644 image/test/crashtests/1241729-1.html create mode 100644 image/test/crashtests/1242093-1.html create mode 100644 image/test/crashtests/1242778-1.png create mode 100644 image/test/crashtests/1249576-1.png create mode 100644 image/test/crashtests/1251091-1.html create mode 100644 image/test/crashtests/1251091-1.png create mode 100644 image/test/crashtests/1253362-1.html create mode 100644 image/test/crashtests/1262549-1.gif create mode 100644 image/test/crashtests/1277397-1.jpg create mode 100644 image/test/crashtests/1277397-2.jpg create mode 100644 image/test/crashtests/1355898-1.html create mode 100644 image/test/crashtests/1375842-1.html create mode 100644 image/test/crashtests/1413762-1.gif create mode 100644 image/test/crashtests/1443232-1.gif create mode 100644 image/test/crashtests/1443232-1.html create mode 100644 image/test/crashtests/1509998.gif create mode 100644 image/test/crashtests/1526717-1.html create mode 100644 image/test/crashtests/1526717-1.png create mode 100644 image/test/crashtests/1629490-1-iframe.html create mode 100644 image/test/crashtests/1629490-1.html create mode 100644 image/test/crashtests/1634839-1-iframe.html create mode 100644 image/test/crashtests/1634839-1.html create mode 100644 image/test/crashtests/1634839-2-iframe.html create mode 100644 image/test/crashtests/1634839-2.html create mode 100644 image/test/crashtests/1676172-1-iframe.html create mode 100644 image/test/crashtests/1676172-1.gif create mode 100644 image/test/crashtests/1676172-1.html create mode 100644 image/test/crashtests/1763581-1-iframe.html create mode 100644 image/test/crashtests/1763581-1.gif create mode 100644 image/test/crashtests/1763581-1.html create mode 100644 image/test/crashtests/1763581-1.sjs create mode 100644 image/test/crashtests/1765871-1.jpg create mode 100644 image/test/crashtests/1814553.avif create mode 100644 image/test/crashtests/1814561.avif create mode 100644 image/test/crashtests/1814677.avif create mode 100644 image/test/crashtests/1814708.avif create mode 100644 image/test/crashtests/1814741.avif create mode 100644 image/test/crashtests/1814774.avif create mode 100644 image/test/crashtests/1817108.avif create mode 100644 image/test/crashtests/256-height.ico create mode 100644 image/test/crashtests/256-width.ico create mode 100644 image/test/crashtests/463696.bmp create mode 100644 image/test/crashtests/523528-1.gif create mode 100644 image/test/crashtests/523528-2.gif create mode 100644 image/test/crashtests/570451.png create mode 100644 image/test/crashtests/694165-1.xhtml create mode 100644 image/test/crashtests/732319-1.html create mode 100644 image/test/crashtests/83804-1.gif create mode 100644 image/test/crashtests/844403-1.html create mode 100644 image/test/crashtests/856616.gif create mode 100644 image/test/crashtests/89341-1.gif create mode 100644 image/test/crashtests/944353.jpg create mode 100644 image/test/crashtests/colormap-range.gif create mode 100644 image/test/crashtests/crashtests.list create mode 100644 image/test/crashtests/delayedframe.sjs create mode 100644 image/test/crashtests/delaytest.html create mode 100644 image/test/crashtests/discardframe.htm create mode 100644 image/test/crashtests/finite-apng.png create mode 100644 image/test/crashtests/ie.png create mode 100644 image/test/crashtests/invalid-disposal-method-1.gif create mode 100644 image/test/crashtests/invalid-disposal-method-2.gif create mode 100644 image/test/crashtests/invalid-disposal-method-3.gif create mode 100644 image/test/crashtests/invalid-icc-profile.jpg create mode 100644 image/test/crashtests/invalid-size-second-frame.gif create mode 100644 image/test/crashtests/invalid-size.gif create mode 100644 image/test/crashtests/invalid_ico_height.ico create mode 100644 image/test/crashtests/invalid_ico_width.ico create mode 100644 image/test/crashtests/multiple-png-hassize.ico create mode 100644 image/test/crashtests/out2.gif create mode 100644 image/test/crashtests/ownerdiscard.html create mode 100644 image/test/crashtests/rainbow.gif create mode 100644 image/test/crashtests/threeframes-end.gif create mode 100644 image/test/crashtests/threeframes-start.gif create mode 100644 image/test/crashtests/truncated-second-frame.png create mode 100644 image/test/crashtests/unsized-svg.svg create mode 100644 image/test/fuzzing/TestDecoders.cpp create mode 100644 image/test/fuzzing/moz.build create mode 100644 image/test/gtest/Common.cpp create mode 100644 image/test/gtest/Common.h create mode 100644 image/test/gtest/TestADAM7InterpolatingFilter.cpp create mode 100644 image/test/gtest/TestAnimationFrameBuffer.cpp create mode 100644 image/test/gtest/TestBlendAnimationFilter.cpp create mode 100644 image/test/gtest/TestCopyOnWrite.cpp create mode 100644 image/test/gtest/TestDecodeToSurface.cpp create mode 100644 image/test/gtest/TestDecoders.cpp create mode 100644 image/test/gtest/TestDecodersPerf.cpp create mode 100644 image/test/gtest/TestDeinterlacingFilter.cpp create mode 100644 image/test/gtest/TestDownscalingFilter.cpp create mode 100644 image/test/gtest/TestFrameAnimator.cpp create mode 100644 image/test/gtest/TestLoader.cpp create mode 100644 image/test/gtest/TestMetadata.cpp create mode 100644 image/test/gtest/TestRemoveFrameRectFilter.cpp create mode 100644 image/test/gtest/TestSourceBuffer.cpp create mode 100644 image/test/gtest/TestStreamingLexer.cpp create mode 100644 image/test/gtest/TestSurfaceCache.cpp create mode 100644 image/test/gtest/TestSurfacePipeIntegration.cpp create mode 100644 image/test/gtest/TestSurfaceSink.cpp create mode 100644 image/test/gtest/TestSwizzleFilter.cpp create mode 100644 image/test/gtest/animated-with-extra-image-sub-blocks.gif create mode 100644 image/test/gtest/blend.avif create mode 100644 image/test/gtest/blend.gif create mode 100644 image/test/gtest/blend.png create mode 100644 image/test/gtest/blend.webp create mode 100644 image/test/gtest/bug-1655846.avif create mode 100644 image/test/gtest/corrupt-with-bad-bmp-height.ico create mode 100644 image/test/gtest/corrupt-with-bad-bmp-width.ico create mode 100644 image/test/gtest/corrupt-with-bad-ico-bpp.ico create mode 100644 image/test/gtest/corrupt.jpg create mode 100644 image/test/gtest/downscaled.avif create mode 100644 image/test/gtest/downscaled.bmp create mode 100644 image/test/gtest/downscaled.gif create mode 100644 image/test/gtest/downscaled.ico create mode 100644 image/test/gtest/downscaled.icon create mode 100644 image/test/gtest/downscaled.jpg create mode 100644 image/test/gtest/downscaled.jxl create mode 100644 image/test/gtest/downscaled.png create mode 100644 image/test/gtest/downscaled.webp create mode 100644 image/test/gtest/exif_resolution.jpg create mode 100644 image/test/gtest/first-frame-green.avif create mode 100644 image/test/gtest/first-frame-green.gif create mode 100644 image/test/gtest/first-frame-green.png create mode 100644 image/test/gtest/first-frame-green.webp create mode 100644 image/test/gtest/first-frame-padding.gif create mode 100644 image/test/gtest/gray-235-10bit-full-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-10bit-full-range-bt601.avif create mode 100644 image/test/gtest/gray-235-10bit-full-range-bt709.avif create mode 100644 image/test/gtest/gray-235-10bit-full-range-grayscale.avif create mode 100644 image/test/gtest/gray-235-10bit-limited-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-10bit-limited-range-bt601.avif create mode 100644 image/test/gtest/gray-235-10bit-limited-range-bt709.avif create mode 100644 image/test/gtest/gray-235-10bit-limited-range-grayscale.avif create mode 100644 image/test/gtest/gray-235-12bit-full-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-12bit-full-range-bt601.avif create mode 100644 image/test/gtest/gray-235-12bit-full-range-bt709.avif create mode 100644 image/test/gtest/gray-235-12bit-full-range-grayscale.avif create mode 100644 image/test/gtest/gray-235-12bit-limited-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-12bit-limited-range-bt601.avif create mode 100644 image/test/gtest/gray-235-12bit-limited-range-bt709.avif create mode 100644 image/test/gtest/gray-235-12bit-limited-range-grayscale.avif create mode 100644 image/test/gtest/gray-235-8bit-full-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-8bit-full-range-bt601.avif create mode 100644 image/test/gtest/gray-235-8bit-full-range-bt709.avif create mode 100644 image/test/gtest/gray-235-8bit-full-range-grayscale.avif create mode 100644 image/test/gtest/gray-235-8bit-limited-range-bt2020.avif create mode 100644 image/test/gtest/gray-235-8bit-limited-range-bt601.avif create mode 100644 image/test/gtest/gray-235-8bit-limited-range-bt709.avif create mode 100644 image/test/gtest/gray-235-8bit-limited-range-grayscale.avif create mode 100644 image/test/gtest/green-1x1-truncated.gif create mode 100644 image/test/gtest/green-large-bmp.ico create mode 100644 image/test/gtest/green-large-png.ico create mode 100644 image/test/gtest/green-multiple-sizes.ico create mode 100644 image/test/gtest/green.avif create mode 100644 image/test/gtest/green.bmp create mode 100644 image/test/gtest/green.gif create mode 100644 image/test/gtest/green.icc_srgb.webp create mode 100644 image/test/gtest/green.ico create mode 100644 image/test/gtest/green.icon create mode 100644 image/test/gtest/green.jpg create mode 100644 image/test/gtest/green.jxl create mode 100644 image/test/gtest/green.png create mode 100644 image/test/gtest/green.webp create mode 100644 image/test/gtest/hdlr-nonzero-reserved-bug-1727033.avif create mode 100644 image/test/gtest/invalid-truncated-metadata.bmp create mode 100644 image/test/gtest/large.avif create mode 100644 image/test/gtest/large.jxl create mode 100644 image/test/gtest/large.webp create mode 100644 image/test/gtest/moz.build create mode 100644 image/test/gtest/multilayer.avif create mode 100644 image/test/gtest/no-frame-delay.gif create mode 100644 image/test/gtest/perf_cmyk.jpg create mode 100644 image/test/gtest/perf_gray.jpg create mode 100644 image/test/gtest/perf_gray.png create mode 100644 image/test/gtest/perf_gray_alpha.png create mode 100644 image/test/gtest/perf_srgb.gif create mode 100644 image/test/gtest/perf_srgb.png create mode 100644 image/test/gtest/perf_srgb_alpha.png create mode 100644 image/test/gtest/perf_srgb_alpha_lossless.webp create mode 100644 image/test/gtest/perf_srgb_alpha_lossy.webp create mode 100644 image/test/gtest/perf_srgb_lossless.webp create mode 100644 image/test/gtest/perf_srgb_lossy.webp create mode 100644 image/test/gtest/perf_ycbcr.jpg create mode 100644 image/test/gtest/rle4.bmp create mode 100644 image/test/gtest/rle8.bmp create mode 100644 image/test/gtest/stackcheck.avif create mode 100644 image/test/gtest/transparent-green-50pct-10bit-yuv420.avif create mode 100644 image/test/gtest/transparent-green-50pct-10bit-yuv422.avif create mode 100644 image/test/gtest/transparent-green-50pct-10bit-yuv444.avif create mode 100644 image/test/gtest/transparent-green-50pct-12bit-yuv420.avif create mode 100644 image/test/gtest/transparent-green-50pct-12bit-yuv422.avif create mode 100644 image/test/gtest/transparent-green-50pct-12bit-yuv444.avif create mode 100644 image/test/gtest/transparent-green-50pct-8bit-yuv420.avif create mode 100644 image/test/gtest/transparent-green-50pct-8bit-yuv422.avif create mode 100644 image/test/gtest/transparent-green-50pct-8bit-yuv444.avif create mode 100644 image/test/gtest/transparent-ico-with-and-mask.ico create mode 100644 image/test/gtest/transparent-if-within-ico.bmp create mode 100644 image/test/gtest/transparent-no-alpha-header.webp create mode 100644 image/test/gtest/transparent.avif create mode 100644 image/test/gtest/transparent.gif create mode 100644 image/test/gtest/transparent.jxl create mode 100644 image/test/gtest/transparent.png create mode 100644 image/test/gtest/transparent.webp create mode 100644 image/test/gtest/valid-avif-colr-nclx-and-prof.avif create mode 100644 image/test/mochitest/12M-pixels-1.png create mode 100644 image/test/mochitest/12M-pixels-2.png create mode 100644 image/test/mochitest/6M-pixels.png create mode 100644 image/test/mochitest/INT32_MIN.bmp create mode 100644 image/test/mochitest/animated-avif.avif create mode 100644 image/test/mochitest/animated-gif-finalframe.gif create mode 100644 image/test/mochitest/animated-gif.gif create mode 100644 image/test/mochitest/animated-gif2.gif create mode 100644 image/test/mochitest/animated-gif_trailing-garbage.gif create mode 100644 image/test/mochitest/animated1.gif create mode 100644 image/test/mochitest/animated1.svg create mode 100644 image/test/mochitest/animated2.gif create mode 100644 image/test/mochitest/animatedMask.gif create mode 100644 image/test/mochitest/animation.svg create mode 100644 image/test/mochitest/animationPolling.js create mode 100644 image/test/mochitest/bad.jpg create mode 100644 image/test/mochitest/big.png create mode 100644 image/test/mochitest/blue.gif create mode 100644 image/test/mochitest/blue.png create mode 100644 image/test/mochitest/bug1132427.gif create mode 100644 image/test/mochitest/bug1132427.html create mode 100644 image/test/mochitest/bug1180105-waiter.sjs create mode 100644 image/test/mochitest/bug1180105.sjs create mode 100644 image/test/mochitest/bug1217571-iframe.html create mode 100644 image/test/mochitest/bug1217571.jpg create mode 100644 image/test/mochitest/bug1319025-ref.png create mode 100644 image/test/mochitest/bug1319025.png create mode 100644 image/test/mochitest/bug399925.gif create mode 100644 image/test/mochitest/bug415761.ico create mode 100644 image/test/mochitest/bug468160.sjs create mode 100644 image/test/mochitest/bug478398_ONLY.png create mode 100644 image/test/mochitest/bug490949-iframe.html create mode 100644 image/test/mochitest/bug490949.sjs create mode 100644 image/test/mochitest/bug496292-1.sjs create mode 100644 image/test/mochitest/bug496292-2.sjs create mode 100644 image/test/mochitest/bug496292-iframe-1.html create mode 100644 image/test/mochitest/bug496292-iframe-2.html create mode 100644 image/test/mochitest/bug496292-iframe-ref.html create mode 100644 image/test/mochitest/bug497665-iframe.html create mode 100644 image/test/mochitest/bug497665.sjs create mode 100644 image/test/mochitest/bug552605.sjs create mode 100644 image/test/mochitest/bug657191.sjs create mode 100644 image/test/mochitest/bug671906-iframe.html create mode 100644 image/test/mochitest/bug671906.sjs create mode 100644 image/test/mochitest/bug733553-informant.sjs create mode 100644 image/test/mochitest/bug733553.sjs create mode 100644 image/test/mochitest/bug767779.sjs create mode 100644 image/test/mochitest/bug89419-iframe.html create mode 100644 image/test/mochitest/bug89419.sjs create mode 100644 image/test/mochitest/bug900200-ref.png create mode 100644 image/test/mochitest/bug900200.png create mode 100644 image/test/mochitest/child.html create mode 100644 image/test/mochitest/chrome.ini create mode 100644 image/test/mochitest/clear.avif create mode 100644 image/test/mochitest/clear.gif create mode 100644 image/test/mochitest/clear.png create mode 100644 image/test/mochitest/clear.webp create mode 100644 image/test/mochitest/clear2-results.gif create mode 100644 image/test/mochitest/clear2.gif create mode 100644 image/test/mochitest/clear2.webp create mode 100644 image/test/mochitest/damon.jpg create mode 100644 image/test/mochitest/error-early.png create mode 100644 image/test/mochitest/filter-final.svg create mode 100644 image/test/mochitest/filter.svg create mode 100644 image/test/mochitest/finite-apng.png create mode 100644 image/test/mochitest/first-frame-padding.gif create mode 100644 image/test/mochitest/green-background.html create mode 100644 image/test/mochitest/green.png create mode 100644 image/test/mochitest/grey.png create mode 100644 image/test/mochitest/ico-bmp-opaque.ico create mode 100644 image/test/mochitest/ico-bmp-transparent.ico create mode 100644 image/test/mochitest/iframe.html create mode 100644 image/test/mochitest/imgutils.js create mode 100644 image/test/mochitest/infinite-apng.png create mode 100644 image/test/mochitest/infinite.avif create mode 100644 image/test/mochitest/infinite.webp create mode 100644 image/test/mochitest/invalid.jpg create mode 100644 image/test/mochitest/keep.gif create mode 100644 image/test/mochitest/keep.png create mode 100644 image/test/mochitest/keep.webp create mode 100644 image/test/mochitest/lime-anim-100x100-2.svg create mode 100644 image/test/mochitest/lime-anim-100x100.svg create mode 100644 image/test/mochitest/lime-css-anim-100x100.svg create mode 100644 image/test/mochitest/lime100x100.svg create mode 100644 image/test/mochitest/mochitest.ini create mode 100644 image/test/mochitest/mq_dynamic_svg_ref.html create mode 100644 image/test/mochitest/mq_dynamic_svg_test.html create mode 100644 image/test/mochitest/opaque.bmp create mode 100644 image/test/mochitest/over.png create mode 100644 image/test/mochitest/purple.gif create mode 100644 image/test/mochitest/rainbow.gif create mode 100644 image/test/mochitest/red.gif create mode 100644 image/test/mochitest/red.png create mode 100644 image/test/mochitest/ref-iframe.html create mode 100644 image/test/mochitest/restore-previous.gif create mode 100644 image/test/mochitest/restore-previous.png create mode 100644 image/test/mochitest/rillybad.jpg create mode 100644 image/test/mochitest/schrep.png create mode 100644 image/test/mochitest/shaver.png create mode 100644 image/test/mochitest/short_header.gif create mode 100644 image/test/mochitest/source.png create mode 100644 image/test/mochitest/test_animSVGImage.html create mode 100644 image/test/mochitest/test_animSVGImage2.html create mode 100644 image/test/mochitest/test_animated_css_image.html create mode 100644 image/test/mochitest/test_animated_gif.html create mode 100644 image/test/mochitest/test_animation.html create mode 100644 image/test/mochitest/test_animation2.html create mode 100644 image/test/mochitest/test_animation_operators.html create mode 100644 image/test/mochitest/test_background_image_anim.html create mode 100644 image/test/mochitest/test_bug1132427.html create mode 100644 image/test/mochitest/test_bug1180105.html create mode 100644 image/test/mochitest/test_bug1217571.html create mode 100644 image/test/mochitest/test_bug1325080.html create mode 100644 image/test/mochitest/test_bug399925.html create mode 100644 image/test/mochitest/test_bug415761.html create mode 100644 image/test/mochitest/test_bug435296.html create mode 100644 image/test/mochitest/test_bug466586.html create mode 100644 image/test/mochitest/test_bug468160.html create mode 100644 image/test/mochitest/test_bug478398.html create mode 100644 image/test/mochitest/test_bug490949.html create mode 100644 image/test/mochitest/test_bug496292.html create mode 100644 image/test/mochitest/test_bug497665.html create mode 100644 image/test/mochitest/test_bug552605-1.html create mode 100644 image/test/mochitest/test_bug552605-2.html create mode 100644 image/test/mochitest/test_bug553982.html create mode 100644 image/test/mochitest/test_bug601470.html create mode 100644 image/test/mochitest/test_bug614392.html create mode 100644 image/test/mochitest/test_bug657191.html create mode 100644 image/test/mochitest/test_bug671906.html create mode 100644 image/test/mochitest/test_bug733553.html create mode 100644 image/test/mochitest/test_bug767779.html create mode 100644 image/test/mochitest/test_bug865919.html create mode 100644 image/test/mochitest/test_bug89419-1.html create mode 100644 image/test/mochitest/test_bug89419-2.html create mode 100644 image/test/mochitest/test_bullet_animation.html create mode 100644 image/test/mochitest/test_canvas_frame_animation.html create mode 100644 image/test/mochitest/test_changeOfSource.html create mode 100644 image/test/mochitest/test_changeOfSource2.html create mode 100644 image/test/mochitest/test_discardAnimatedImage.html create mode 100644 image/test/mochitest/test_discardFinishedAnimatedImage.html create mode 100644 image/test/mochitest/test_discardFramesAnimatedImage.html create mode 100644 image/test/mochitest/test_drawDiscardedImage.html create mode 100644 image/test/mochitest/test_error_events.html create mode 100644 image/test/mochitest/test_has_transparency.html create mode 100644 image/test/mochitest/test_image_cache_notification.html create mode 100644 image/test/mochitest/test_image_crossorigin_data_url.html create mode 100644 image/test/mochitest/test_mq_dynamic_svg.html create mode 100644 image/test/mochitest/test_net_failedtoprocess.html create mode 100644 image/test/mochitest/test_removal_ondecode.html create mode 100644 image/test/mochitest/test_removal_onload.html create mode 100644 image/test/mochitest/test_short_gif_header.html create mode 100644 image/test/mochitest/test_staticClone.html create mode 100644 image/test/mochitest/test_svg_animatedGIF.html create mode 100644 image/test/mochitest/test_svg_filter_animation.html create mode 100644 image/test/mochitest/test_synchronized_animation.html create mode 100644 image/test/mochitest/test_undisplayed_iframe.html create mode 100644 image/test/mochitest/test_webcam.html create mode 100644 image/test/mochitest/test_xultree_animation.xhtml create mode 100644 image/test/mochitest/transparent.gif create mode 100644 image/test/mochitest/transparent.png create mode 100644 image/test/mochitest/webcam-simulacrum.sjs create mode 100644 image/test/reftest/ImageDocument.css create mode 100644 image/test/reftest/apng/bug411852-1-ref.html create mode 100644 image/test/reftest/apng/bug411852-1-ref.png create mode 100644 image/test/reftest/apng/bug411852-1.png create mode 100644 image/test/reftest/apng/bug546272-ref.html create mode 100644 image/test/reftest/apng/bug546272-ref.png create mode 100644 image/test/reftest/apng/bug546272.png create mode 100644 image/test/reftest/apng/delaytest.html create mode 100644 image/test/reftest/apng/reftest.list create mode 100644 image/test/reftest/avif/1-normal.avif create mode 100644 image/test/reftest/avif/2-flipped-horizontally.avif create mode 100644 image/test/reftest/avif/3-rotated-180deg.avif create mode 100644 image/test/reftest/avif/4-flipped-vertically.avif create mode 100644 image/test/reftest/avif/5-rotated-90deg-CCW-and-flipped-vertically.avif create mode 100644 image/test/reftest/avif/6-rotated-90deg-CCW.avif create mode 100644 image/test/reftest/avif/7-rotated-90deg-CW-and-flipped-vertically.avif create mode 100644 image/test/reftest/avif/8-rotated-90deg-CW.avif create mode 100644 image/test/reftest/avif/img_irot0_imir0.avif create mode 100644 image/test/reftest/avif/img_irot0_imir1.avif create mode 100644 image/test/reftest/avif/img_irot0_imirN.avif create mode 100644 image/test/reftest/avif/img_irot1_imir0.avif create mode 100644 image/test/reftest/avif/img_irot1_imir1.avif create mode 100644 image/test/reftest/avif/img_irot1_imirN.avif create mode 100644 image/test/reftest/avif/img_irot2_imir0.avif create mode 100644 image/test/reftest/avif/img_irot2_imir1.avif create mode 100644 image/test/reftest/avif/img_irot2_imirN.avif create mode 100644 image/test/reftest/avif/img_irot3_imir0.avif create mode 100644 image/test/reftest/avif/img_irot3_imir1.avif create mode 100644 image/test/reftest/avif/img_irot3_imirN.avif create mode 100644 image/test/reftest/avif/img_irotN_imir0.avif create mode 100644 image/test/reftest/avif/img_irotN_imir1.avif create mode 100644 image/test/reftest/avif/img_irotN_imirN.avif create mode 100644 image/test/reftest/avif/reftest.list create mode 100644 image/test/reftest/bmp/1240629-1.bmp create mode 100644 image/test/reftest/bmp/1240629-2.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.ico create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.png create mode 100644 image/test/reftest/bmp/bmp-1bpp/os2bmp-size-32x32-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-1bpp/reftest.list create mode 100644 image/test/reftest/bmp/bmp-1bpp/top-to-bottom-16x16-1bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.png create mode 100644 image/test/reftest/bmp/bmp-24bpp/os2bmp-size-32x32-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-24bpp/reftest.list create mode 100644 image/test/reftest/bmp/bmp-24bpp/top-to-bottom-16x16-24bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/os2bmp-size-32x32-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/reftest.list create mode 100644 image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.bmp create mode 100644 image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.png create mode 100644 image/test/reftest/bmp/bmp-4bpp/top-to-bottom-16x16-4bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-extrapad-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.png create mode 100644 image/test/reftest/bmp/bmp-8bpp/os2-bmp-size-32x32-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/reftest.list create mode 100644 image/test/reftest/bmp/bmp-8bpp/rle-bmp-extrapad-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/rle-bmp-not-square-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/rle-bmp-size-32x32-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/top-to-bottom-16x16-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-8bpp/top-to-bottom-rle-bmp-size-32x32-8bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-compression-BITFIELDS.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE4.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE8.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-compression.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-data-offset.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-signature.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/invalid-truncated-metadata.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/os2-invalid-bpp.bmp create mode 100644 image/test/reftest/bmp/bmp-corrupted/reftest.list create mode 100644 image/test/reftest/bmp/bmp-corrupted/wrapper.html create mode 100644 image/test/reftest/bmp/bmpsuite/COPYING.txt create mode 100644 image/test/reftest/bmp/bmpsuite/README.mozilla create mode 100644 image/test/reftest/bmp/bmpsuite/b/badbitcount.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badbitssize.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/baddens1.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/baddens2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badfilesize.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badheadersize.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badpalettesize.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badplanes.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4bis.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4bis.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4ter.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrle4ter.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrlebis.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrlebis.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrleter.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/badrleter.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/badwidth.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/pal1.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/pal8.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/pal8badindex.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/pal8badindex.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/reallybig.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/reftest.list create mode 100644 image/test/reftest/bmp/bmpsuite/b/rgb16-880.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/rgb16-880.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/rletopdown.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/shortfile.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/b/shortfile.png create mode 100644 image/test/reftest/bmp/bmpsuite/b/wrapper.html create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal1.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal1.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal1bg.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal1bg.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal1wb.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal4.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal4.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal4gs.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal4gs.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal4rle.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8-0.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8gs.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8gs.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8nonsquare-e.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8os2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8rle.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8topdown.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8v4.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8v5.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w124.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w124.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w125.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w125.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w126.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/pal8w126.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/reftest.list create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16-565.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16-565.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16-565pal.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb16bfdef.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb24.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb24.png create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb24pal.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb32.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb32bf.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/g/rgb32bfdef.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal1huff.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal1p1.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal1p1.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal2color.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal4rlecut.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal4rlecut.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal4rletrns.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal4rletrns.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8offs.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2-hs.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2-sz.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2sp.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2v2-16.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2v2-40sz.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2v2-sz.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8os2v2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8oversizepal.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8rlecut.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8rlecut.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8rletrns.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/pal8rletrns.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/reftest.list create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16-231.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16-231.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16-3103.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16-3103.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb16faketrns.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24jpeg.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24largepal.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24lprof.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24png.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24prof.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24prof2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24rle24.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32-111110.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32-7187.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32-7187.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32-xbgr.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32fakealpha.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb32h52.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-1924.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-1924.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-4444.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-4444.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-5551.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba16-5551.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-1.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-2.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-61754.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-61754.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-81284.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32-81284.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32.png create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32abf.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/rgba32h56.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/q/wrapper.html create mode 100644 image/test/reftest/bmp/bmpsuite/reftest.list create mode 100644 image/test/reftest/bmp/bmpsuite/x/ba-bm.bmp create mode 100644 image/test/reftest/bmp/bmpsuite/x/reftest.list create mode 100644 image/test/reftest/bmp/bmpsuite/x/wrapper.html create mode 100644 image/test/reftest/bmp/reftest.list create mode 100644 image/test/reftest/color-management/color-curv.png create mode 100644 image/test/reftest/color-management/color-lin.png create mode 100644 image/test/reftest/color-management/color-table.png create mode 100644 image/test/reftest/color-management/invalid-chrm-ref.png create mode 100644 image/test/reftest/color-management/invalid-chrm.png create mode 100644 image/test/reftest/color-management/invalid-whitepoint.png create mode 100644 image/test/reftest/color-management/reftest.list create mode 100644 image/test/reftest/color-management/trc-type-ref.html create mode 100644 image/test/reftest/color-management/trc-type.html create mode 100644 image/test/reftest/colordepth.html create mode 100644 image/test/reftest/downscaling/100x100.gif create mode 100644 image/test/reftest/downscaling/100x100.jpg create mode 100644 image/test/reftest/downscaling/100x100.png create mode 100644 image/test/reftest/downscaling/100x32768.gif create mode 100644 image/test/reftest/downscaling/100x32768.jpg create mode 100644 image/test/reftest/downscaling/100x32768.png create mode 100644 image/test/reftest/downscaling/1404366-1.html create mode 100644 image/test/reftest/downscaling/1404366-1.ico create mode 100644 image/test/reftest/downscaling/1421191-1.html create mode 100644 image/test/reftest/downscaling/1421191-1.png create mode 100644 image/test/reftest/downscaling/32768x100.gif create mode 100644 image/test/reftest/downscaling/32768x100.jpg create mode 100644 image/test/reftest/downscaling/32768x100.png create mode 100644 image/test/reftest/downscaling/black-border-bottom.png create mode 100644 image/test/reftest/downscaling/black-border-left.png create mode 100644 image/test/reftest/downscaling/black-border-rect.svg create mode 100644 image/test/reftest/downscaling/black-border-right.png create mode 100644 image/test/reftest/downscaling/black-border-top.png create mode 100644 image/test/reftest/downscaling/bmp-size-16x16-24bpp.png create mode 100644 image/test/reftest/downscaling/downscale-1-bigimage.png create mode 100644 image/test/reftest/downscaling/downscale-1-ref.html create mode 100644 image/test/reftest/downscaling/downscale-1-smallimage.png create mode 100644 image/test/reftest/downscaling/downscale-1.html create mode 100644 image/test/reftest/downscaling/downscale-16px.html create mode 100644 image/test/reftest/downscaling/downscale-2a.html create mode 100644 image/test/reftest/downscaling/downscale-2b.html create mode 100644 image/test/reftest/downscaling/downscale-2c.html create mode 100644 image/test/reftest/downscaling/downscale-2d.html create mode 100644 image/test/reftest/downscaling/downscale-2e.html create mode 100644 image/test/reftest/downscaling/downscale-2f.html create mode 100644 image/test/reftest/downscaling/downscale-32px-ref.html create mode 100644 image/test/reftest/downscaling/downscale-32px.html create mode 100644 image/test/reftest/downscaling/downscale-8px.html create mode 100644 image/test/reftest/downscaling/downscale-moz-icon-1-ref.html create mode 100644 image/test/reftest/downscaling/downscale-moz-icon-1.html create mode 100644 image/test/reftest/downscaling/downscale-orient-ref.html create mode 100644 image/test/reftest/downscaling/downscale-orient-ref.png create mode 100644 image/test/reftest/downscaling/downscale-orient.html create mode 100644 image/test/reftest/downscaling/downscale-png.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1-ref.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1a.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1b.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1c.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1d.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1e.html create mode 100644 image/test/reftest/downscaling/downscale-svg-1f.html create mode 100644 image/test/reftest/downscaling/ff-0RGB.ico create mode 100644 image/test/reftest/downscaling/ff-0RGB.png create mode 100644 image/test/reftest/downscaling/ff-ARGB.ico create mode 100644 image/test/reftest/downscaling/ff-ARGB.png create mode 100644 image/test/reftest/downscaling/huge-1.html create mode 100644 image/test/reftest/downscaling/image-pre-rotated-90-deg.jpg create mode 100644 image/test/reftest/downscaling/lime-red-256px-bmp-in.ico create mode 100644 image/test/reftest/downscaling/lime-red-256px-png-in.ico create mode 100644 image/test/reftest/downscaling/lime-red-256px.bmp create mode 100644 image/test/reftest/downscaling/lime-red-256px.gif create mode 100644 image/test/reftest/downscaling/lime-red-256px.jpg create mode 100644 image/test/reftest/downscaling/lime-red-256px.png create mode 100644 image/test/reftest/downscaling/lime-red-256px.svg create mode 100644 image/test/reftest/downscaling/lime-red-32px.png create mode 100644 image/test/reftest/downscaling/png-interlaced.png create mode 100644 image/test/reftest/downscaling/png-normal.png create mode 100644 image/test/reftest/downscaling/reftest.list create mode 100644 image/test/reftest/downscaling/top-to-bottom-16x16-24bpp.bmp create mode 100644 image/test/reftest/encoders-lossless/ImageDocument.css create mode 100644 image/test/reftest/encoders-lossless/encoder.html create mode 100644 image/test/reftest/encoders-lossless/reftest.list create mode 100644 image/test/reftest/encoders-lossless/size-15x15.png create mode 100644 image/test/reftest/encoders-lossless/size-16x16.png create mode 100644 image/test/reftest/encoders-lossless/size-17x17.png create mode 100644 image/test/reftest/encoders-lossless/size-1x1.png create mode 100644 image/test/reftest/encoders-lossless/size-256x256.png create mode 100644 image/test/reftest/encoders-lossless/size-2x2.png create mode 100644 image/test/reftest/encoders-lossless/size-31x31.png create mode 100644 image/test/reftest/encoders-lossless/size-32x32.png create mode 100644 image/test/reftest/encoders-lossless/size-33x33.png create mode 100644 image/test/reftest/encoders-lossless/size-3x3.png create mode 100644 image/test/reftest/encoders-lossless/size-4x4.png create mode 100644 image/test/reftest/encoders-lossless/size-5x5.png create mode 100644 image/test/reftest/encoders-lossless/size-6x6.png create mode 100644 image/test/reftest/encoders-lossless/size-7x7.png create mode 100644 image/test/reftest/encoders-lossless/size-8x8.png create mode 100644 image/test/reftest/encoders-lossless/size-9x9.png create mode 100644 image/test/reftest/encoders-lossless/test.png create mode 100644 image/test/reftest/generic/accept-image-catchall-ref.html create mode 100644 image/test/reftest/generic/accept-image-catchall.html create mode 100644 image/test/reftest/generic/check-header.sjs create mode 100644 image/test/reftest/generic/green.png create mode 100644 image/test/reftest/generic/moz-icon-1.html create mode 100644 image/test/reftest/generic/moz-icon-blank-1-almostref.html create mode 100644 image/test/reftest/generic/moz-icon-blank-1-antiref.html create mode 100644 image/test/reftest/generic/moz-icon-blank-1-antiref2.html create mode 100644 image/test/reftest/generic/moz-icon-blank-1-ref.html create mode 100644 image/test/reftest/generic/moz-icon-blank-1.html create mode 100644 image/test/reftest/generic/reftest.list create mode 100644 image/test/reftest/gif/1bit-255-trans.gif create mode 100644 image/test/reftest/gif/1bit-255-trans.png create mode 100644 image/test/reftest/gif/ImageDocument.css create mode 100644 image/test/reftest/gif/animation1a.gif create mode 100644 image/test/reftest/gif/animation2a-finalframe.gif create mode 100644 image/test/reftest/gif/animation2a.gif create mode 100644 image/test/reftest/gif/blue.gif create mode 100644 image/test/reftest/gif/comment.gif create mode 100644 image/test/reftest/gif/comment.png create mode 100644 image/test/reftest/gif/delaytest.html create mode 100644 image/test/reftest/gif/in-colormap-trans.gif create mode 100644 image/test/reftest/gif/in-colormap-trans.png create mode 100644 image/test/reftest/gif/one-color-offset-ref.gif create mode 100644 image/test/reftest/gif/one-color-offset.gif create mode 100644 image/test/reftest/gif/one-pixel-no-image-data-ref.html create mode 100644 image/test/reftest/gif/one-pixel-no-image-data.html create mode 100644 image/test/reftest/gif/out-of-colormap-trans.gif create mode 100644 image/test/reftest/gif/out-of-colormap-trans.png create mode 100644 image/test/reftest/gif/red.gif create mode 100644 image/test/reftest/gif/reftest.list create mode 100644 image/test/reftest/gif/small-background-size-2-ref.gif create mode 100644 image/test/reftest/gif/small-background-size-2.gif create mode 100644 image/test/reftest/gif/small-background-size-ref.gif create mode 100644 image/test/reftest/gif/small-background-size.gif create mode 100644 image/test/reftest/gif/test_bug641198.html create mode 100644 image/test/reftest/gif/tile-transform-ref.html create mode 100644 image/test/reftest/gif/tile-transform.html create mode 100644 image/test/reftest/gif/tiletest-ref.png create mode 100644 image/test/reftest/gif/tiletest.gif create mode 100644 image/test/reftest/gif/transparent-animation-finalframe.gif create mode 100644 image/test/reftest/gif/transparent-animation-finalframe.html create mode 100644 image/test/reftest/gif/transparent-animation.gif create mode 100644 image/test/reftest/gif/truncated-framerect-interlaced-ref.gif create mode 100644 image/test/reftest/gif/truncated-framerect-interlaced.gif create mode 100644 image/test/reftest/gif/truncated-framerect-ref.gif create mode 100644 image/test/reftest/gif/truncated-framerect-ref.html create mode 100644 image/test/reftest/gif/truncated-framerect.gif create mode 100644 image/test/reftest/gif/truncated-framerect.html create mode 100644 image/test/reftest/ico/cur/pointer.cur create mode 100644 image/test/reftest/ico/cur/pointer.png create mode 100644 image/test/reftest/ico/cur/reftest.list create mode 100644 image/test/reftest/ico/cur/wrapper.html create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-1bpp/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-24bpp/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-32bpp/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-4bpp/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.png create mode 100644 image/test/reftest/ico/ico-bmp-8bpp/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/16x16.png create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/invalid-bpp.ico create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE4.ico create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE8.ico create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/invalid-compression.ico create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/reftest.list create mode 100644 image/test/reftest/ico/ico-bmp-corrupted/wrapper.html create mode 100644 image/test/reftest/ico/ico-mixed/mixed-bmp-png.ico create mode 100644 image/test/reftest/ico/ico-mixed/mixed-bmp-png.png create mode 100644 image/test/reftest/ico/ico-mixed/mixed-bmp-png32.png create mode 100644 image/test/reftest/ico/ico-mixed/mixed-bmp-png48.png create mode 100644 image/test/reftest/ico/ico-mixed/reftest.list create mode 100644 image/test/reftest/ico/ico-png/corrupted_x00n0g01.ico create mode 100644 image/test/reftest/ico/ico-png/corrupted_xxcrn0g04.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-15x15-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-15x15-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-16x16-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-16x16-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-17x17-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-17x17-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-1x1-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-1x1-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-256x256-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-256x256-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-2x2-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-2x2-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-31x31-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-31x31-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-32x32-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-32x32-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-33x33-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-33x33-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-3x3-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-3x3-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-4x4-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-4x4-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-5x5-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-5x5-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-6x6-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-6x6-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-7x7-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-7x7-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-8x8-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-8x8-png.png create mode 100644 image/test/reftest/ico/ico-png/ico-size-9x9-png.ico create mode 100644 image/test/reftest/ico/ico-png/ico-size-9x9-png.png create mode 100644 image/test/reftest/ico/ico-png/reftest.list create mode 100644 image/test/reftest/ico/ico-png/tmp.ico create mode 100644 image/test/reftest/ico/ico-png/transparent-png.ico create mode 100644 image/test/reftest/ico/ico-png/transparent-png.png create mode 100644 image/test/reftest/ico/ico-png/wrapper.html create mode 100644 image/test/reftest/ico/ico-png/x00n0g01.png create mode 100644 image/test/reftest/ico/ico-png/xcrn0g04.png create mode 100644 image/test/reftest/ico/reftest.list create mode 100644 image/test/reftest/img2html.html create mode 100644 image/test/reftest/jpeg/blue.html create mode 100644 image/test/reftest/jpeg/blue.jpg create mode 100644 image/test/reftest/jpeg/jpg-cmyk-1.jpg create mode 100644 image/test/reftest/jpeg/jpg-cmyk-1.png create mode 100644 image/test/reftest/jpeg/jpg-cmyk-2.jpg create mode 100644 image/test/reftest/jpeg/jpg-cmyk-2.png create mode 100644 image/test/reftest/jpeg/jpg-gray.jpg create mode 100644 image/test/reftest/jpeg/jpg-gray.png create mode 100644 image/test/reftest/jpeg/jpg-progressive-1000-ref.html create mode 100644 image/test/reftest/jpeg/jpg-progressive-1000.html create mode 100644 image/test/reftest/jpeg/jpg-progressive-1000.jpg create mode 100644 image/test/reftest/jpeg/jpg-progressive.jpg create mode 100644 image/test/reftest/jpeg/jpg-progressive.png create mode 100644 image/test/reftest/jpeg/jpg-size-15x15.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-15x15.png create mode 100644 image/test/reftest/jpeg/jpg-size-16x16.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-16x16.png create mode 100644 image/test/reftest/jpeg/jpg-size-17x17.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-17x17.png create mode 100644 image/test/reftest/jpeg/jpg-size-1x1.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-1x1.png create mode 100644 image/test/reftest/jpeg/jpg-size-2x2.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-2x2.png create mode 100644 image/test/reftest/jpeg/jpg-size-31x31.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-31x31.png create mode 100644 image/test/reftest/jpeg/jpg-size-32x32.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-32x32.png create mode 100644 image/test/reftest/jpeg/jpg-size-33x33.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-33x33.png create mode 100644 image/test/reftest/jpeg/jpg-size-3x3.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-3x3.png create mode 100644 image/test/reftest/jpeg/jpg-size-4x4.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-4x4.png create mode 100644 image/test/reftest/jpeg/jpg-size-5x5.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-5x5.png create mode 100644 image/test/reftest/jpeg/jpg-size-6x6.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-6x6.png create mode 100644 image/test/reftest/jpeg/jpg-size-7x7.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-7x7.png create mode 100644 image/test/reftest/jpeg/jpg-size-8x8.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-8x8.png create mode 100644 image/test/reftest/jpeg/jpg-size-9x9.jpg create mode 100644 image/test/reftest/jpeg/jpg-size-9x9.png create mode 100644 image/test/reftest/jpeg/jpg-srgb-icc.jpg create mode 100644 image/test/reftest/jpeg/jpg-srgb-icc.png create mode 100644 image/test/reftest/jpeg/non-interleaved_progressive-1-halfred-ref.png create mode 100644 image/test/reftest/jpeg/non-interleaved_progressive-1.jpg create mode 100644 image/test/reftest/jpeg/non-interleaved_progressive-2-white-ref.png create mode 100644 image/test/reftest/jpeg/non-interleaved_progressive-2.jpg create mode 100644 image/test/reftest/jpeg/red-bad-marker.jpg create mode 100644 image/test/reftest/jpeg/red.jpg create mode 100644 image/test/reftest/jpeg/reftest.list create mode 100644 image/test/reftest/jpeg/webcam-simulacrum.html create mode 100644 image/test/reftest/jpeg/webcam-simulacrum.mjpg create mode 100644 image/test/reftest/jpeg/webcam-simulacrum.mjpg^headers^ create mode 100644 image/test/reftest/jxl/jxl-size-33x33.jxl create mode 100644 image/test/reftest/jxl/jxl-size-33x33.png create mode 100644 image/test/reftest/jxl/reftest.list create mode 100644 image/test/reftest/pngsuite-ancillary/ccwn2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/ccwn2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/ccwn3p08.html create mode 100644 image/test/reftest/pngsuite-ancillary/ccwn3p08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cdfn2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cdfn2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cdhn2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cdhn2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cdsn2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cdsn2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cdun2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cdun2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/ch1n3p04.html create mode 100644 image/test/reftest/pngsuite-ancillary/ch1n3p04.png create mode 100644 image/test/reftest/pngsuite-ancillary/ch2n3p08.html create mode 100644 image/test/reftest/pngsuite-ancillary/ch2n3p08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cm0n0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/cm0n0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/cm7n0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/cm7n0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/cm9n0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/cm9n0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs3n2c16.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs3n2c16.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs3n3p08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs3n3p08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs5n2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs5n2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs5n3p08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs5n3p08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs8n2c08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs8n2c08.png create mode 100644 image/test/reftest/pngsuite-ancillary/cs8n3p08.html create mode 100644 image/test/reftest/pngsuite-ancillary/cs8n3p08.png create mode 100644 image/test/reftest/pngsuite-ancillary/ct0n0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/ct0n0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/ct1n0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/ct1n0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/ctzn0g04.html create mode 100644 image/test/reftest/pngsuite-ancillary/ctzn0g04.png create mode 100644 image/test/reftest/pngsuite-ancillary/qcms-asm-check.js create mode 100644 image/test/reftest/pngsuite-ancillary/reftest.list create mode 100644 image/test/reftest/pngsuite-background/bg__4a08.html create mode 100644 image/test/reftest/pngsuite-background/bg__4a16.html create mode 100644 image/test/reftest/pngsuite-background/bg__6a08.html create mode 100644 image/test/reftest/pngsuite-background/bg__6a16.html create mode 100644 image/test/reftest/pngsuite-background/bgai4a08.png create mode 100644 image/test/reftest/pngsuite-background/bgai4a16.png create mode 100644 image/test/reftest/pngsuite-background/bgan6a08.png create mode 100644 image/test/reftest/pngsuite-background/bgan6a16.png create mode 100644 image/test/reftest/pngsuite-background/bgbn4a08.png create mode 100644 image/test/reftest/pngsuite-background/bggn4a16.png create mode 100644 image/test/reftest/pngsuite-background/bgwn6a08.png create mode 100644 image/test/reftest/pngsuite-background/bgyn6a16.png create mode 100644 image/test/reftest/pngsuite-background/reftest.list create mode 100644 image/test/reftest/pngsuite-background/wrapper.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g01.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g01.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g02.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g02.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g04.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g04.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g08.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g08.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g16.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi0g16.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi2c08.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi2c08.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi2c16.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi2c16.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p01.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p01.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p02.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p02.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p04.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p04.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p08.html create mode 100644 image/test/reftest/pngsuite-basic-i/basi3p08.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi4a08.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi4a16.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi6a08.png create mode 100644 image/test/reftest/pngsuite-basic-i/basi6a16.png create mode 100644 image/test/reftest/pngsuite-basic-i/reftest.list create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g01.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g01.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g02.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g02.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g04.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g04.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g08.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g08.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g16.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn0g16.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn2c08.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn2c08.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn2c16.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn2c16.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p01.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p01.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p02.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p02.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p04.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p04.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p08.html create mode 100644 image/test/reftest/pngsuite-basic-n/basn3p08.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn4a08.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn4a16.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn6a08.png create mode 100644 image/test/reftest/pngsuite-basic-n/basn6a16.png create mode 100644 image/test/reftest/pngsuite-basic-n/reftest.list create mode 100644 image/test/reftest/pngsuite-chunkorder/color.html create mode 100644 image/test/reftest/pngsuite-chunkorder/grayscale.html create mode 100644 image/test/reftest/pngsuite-chunkorder/oi1n0g16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi1n2c16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi2n0g16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi2n2c16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi4n0g16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi4n2c16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi9n0g16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/oi9n2c16.png create mode 100644 image/test/reftest/pngsuite-chunkorder/reftest.list create mode 100644 image/test/reftest/pngsuite-corrupted/reftest.list create mode 100644 image/test/reftest/pngsuite-corrupted/wrapper.html create mode 100644 image/test/reftest/pngsuite-corrupted/x00n0g01.png create mode 100644 image/test/reftest/pngsuite-corrupted/xcrn0g04.png create mode 100644 image/test/reftest/pngsuite-corrupted/xlfn0g04.png create mode 100644 image/test/reftest/pngsuite-filtering/f00n0g08.html create mode 100644 image/test/reftest/pngsuite-filtering/f00n0g08.png create mode 100644 image/test/reftest/pngsuite-filtering/f00n2c08.html create mode 100644 image/test/reftest/pngsuite-filtering/f00n2c08.png create mode 100644 image/test/reftest/pngsuite-filtering/f01n0g08.html create mode 100644 image/test/reftest/pngsuite-filtering/f01n0g08.png create mode 100644 image/test/reftest/pngsuite-filtering/f01n2c08.html create mode 100644 image/test/reftest/pngsuite-filtering/f01n2c08.png create mode 100644 image/test/reftest/pngsuite-filtering/f02n0g08.html create mode 100644 image/test/reftest/pngsuite-filtering/f02n0g08.png create mode 100644 image/test/reftest/pngsuite-filtering/f02n2c08.html create mode 100644 image/test/reftest/pngsuite-filtering/f02n2c08.png create mode 100644 image/test/reftest/pngsuite-filtering/f03n0g08.html create mode 100644 image/test/reftest/pngsuite-filtering/f03n0g08.png create mode 100644 image/test/reftest/pngsuite-filtering/f03n2c08.html create mode 100644 image/test/reftest/pngsuite-filtering/f03n2c08.png create mode 100644 image/test/reftest/pngsuite-filtering/f04n0g08.html create mode 100644 image/test/reftest/pngsuite-filtering/f04n0g08.png create mode 100644 image/test/reftest/pngsuite-filtering/f04n2c08.html create mode 100644 image/test/reftest/pngsuite-filtering/f04n2c08.png create mode 100644 image/test/reftest/pngsuite-filtering/reftest.list create mode 100644 image/test/reftest/pngsuite-gamma/g03n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g03n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g03n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g03n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g03n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g03n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/g04n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g04n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g04n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g04n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g04n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g04n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/g05n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g05n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g05n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g05n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g05n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g05n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/g07n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g07n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g07n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g07n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g07n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g07n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/g10n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g10n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g10n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g10n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g10n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g10n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/g25n0g16.html create mode 100644 image/test/reftest/pngsuite-gamma/g25n0g16.png create mode 100644 image/test/reftest/pngsuite-gamma/g25n2c08.html create mode 100644 image/test/reftest/pngsuite-gamma/g25n2c08.png create mode 100644 image/test/reftest/pngsuite-gamma/g25n3p04.html create mode 100644 image/test/reftest/pngsuite-gamma/g25n3p04.png create mode 100644 image/test/reftest/pngsuite-gamma/reftest.list create mode 100644 image/test/reftest/pngsuite-oddsizes/reftest.list create mode 100644 image/test/reftest/pngsuite-oddsizes/s01_3p01.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s01i3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s01n3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s02_3p01.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s02i3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s02n3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s03_3p01.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s03i3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s03n3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s04_3p01.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s04i3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s04n3p01.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s05_3p02.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s05i3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s05n3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s06_3p02.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s06i3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s06n3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s07_3p02.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s07i3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s07n3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s08_3p02.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s08i3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s08n3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s09_3p02.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s09i3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s09n3p02.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s32_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s32i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s32n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s33_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s33i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s33n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s34_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s34i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s34n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s35_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s35i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s35n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s36_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s36i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s36n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s37_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s37i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s37n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s38_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s38i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s38n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s39_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s39i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s39n3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s40_3p04.html create mode 100644 image/test/reftest/pngsuite-oddsizes/s40i3p04.png create mode 100644 image/test/reftest/pngsuite-oddsizes/s40n3p04.png create mode 100644 image/test/reftest/pngsuite-palettes/pp0n2c16.html create mode 100644 image/test/reftest/pngsuite-palettes/pp0n2c16.png create mode 100644 image/test/reftest/pngsuite-palettes/pp0n6a08.png create mode 100644 image/test/reftest/pngsuite-palettes/ps1n0g08.html create mode 100644 image/test/reftest/pngsuite-palettes/ps1n0g08.png create mode 100644 image/test/reftest/pngsuite-palettes/ps1n2c16.html create mode 100644 image/test/reftest/pngsuite-palettes/ps1n2c16.png create mode 100644 image/test/reftest/pngsuite-palettes/ps2n0g08.html create mode 100644 image/test/reftest/pngsuite-palettes/ps2n0g08.png create mode 100644 image/test/reftest/pngsuite-palettes/ps2n2c16.html create mode 100644 image/test/reftest/pngsuite-palettes/ps2n2c16.png create mode 100644 image/test/reftest/pngsuite-palettes/reftest.list create mode 100644 image/test/reftest/pngsuite-zlib/reftest.list create mode 100644 image/test/reftest/pngsuite-zlib/z00n2c08.html create mode 100644 image/test/reftest/pngsuite-zlib/z00n2c08.png create mode 100644 image/test/reftest/pngsuite-zlib/z03n2c08.html create mode 100644 image/test/reftest/pngsuite-zlib/z03n2c08.png create mode 100644 image/test/reftest/pngsuite-zlib/z06n2c08.html create mode 100644 image/test/reftest/pngsuite-zlib/z06n2c08.png create mode 100644 image/test/reftest/pngsuite-zlib/z09n2c08.html create mode 100644 image/test/reftest/pngsuite-zlib/z09n2c08.png create mode 100644 image/test/reftest/reftest.list create mode 100644 image/test/reftest/webp/blue.png create mode 100644 image/test/reftest/webp/icc-bit-no-icc-chunk.webp create mode 100644 image/test/reftest/webp/reftest.list create mode 100644 image/test/unit/async_load_tests.js create mode 100644 image/test/unit/bug413512.ico create mode 100644 image/test/unit/bug815359.ico create mode 100644 image/test/unit/image1.png create mode 100644 image/test/unit/image1.webp create mode 100644 image/test/unit/image1png16x16.jpg create mode 100644 image/test/unit/image1png64x64.jpg create mode 100644 image/test/unit/image1quality50.webp create mode 100644 image/test/unit/image2.jpg create mode 100644 image/test/unit/image2jpg16x16-win.png create mode 100644 image/test/unit/image2jpg16x16.png create mode 100644 image/test/unit/image2jpg16x16cropped.jpg create mode 100644 image/test/unit/image2jpg16x16cropped2.jpg create mode 100644 image/test/unit/image2jpg16x32cropped3.jpg create mode 100644 image/test/unit/image2jpg16x32scaled.jpg create mode 100644 image/test/unit/image2jpg32x16cropped4.jpg create mode 100644 image/test/unit/image2jpg32x16scaled.jpg create mode 100644 image/test/unit/image2jpg32x32-win.png create mode 100644 image/test/unit/image2jpg32x32.jpg create mode 100644 image/test/unit/image2jpg32x32.png create mode 100644 image/test/unit/image3.ico create mode 100644 image/test/unit/image3ico16x16.png create mode 100644 image/test/unit/image3ico32x32.png create mode 100644 image/test/unit/image4.gif create mode 100644 image/test/unit/image4gif16x16bmp24bpp.ico create mode 100644 image/test/unit/image4gif16x16bmp32bpp.ico create mode 100644 image/test/unit/image4gif32x32bmp24bpp.ico create mode 100644 image/test/unit/image4gif32x32bmp32bpp.ico create mode 100644 image/test/unit/image_load_helpers.js create mode 100644 image/test/unit/test_async_notification.js create mode 100644 image/test/unit/test_async_notification_404.js create mode 100644 image/test/unit/test_async_notification_animated.js create mode 100644 image/test/unit/test_encoder_apng.js create mode 100644 image/test/unit/test_encoder_png.js create mode 100644 image/test/unit/test_imgtools.js create mode 100644 image/test/unit/test_moz_icon_uri.js create mode 100644 image/test/unit/test_private_channel.js create mode 100644 image/test/unit/xpcshell.ini (limited to 'image') diff --git a/image/AnimationFrameBuffer.cpp b/image/AnimationFrameBuffer.cpp new file mode 100644 index 0000000000..6ef855bbd0 --- /dev/null +++ b/image/AnimationFrameBuffer.cpp @@ -0,0 +1,471 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AnimationFrameBuffer.h" + +#include // for Move + +namespace mozilla { +namespace image { + +AnimationFrameRetainedBuffer::AnimationFrameRetainedBuffer(size_t aThreshold, + size_t aBatch, + size_t aStartFrame) + : AnimationFrameBuffer(aBatch, aStartFrame), mThreshold(aThreshold) { + // To simplify the code, we have the assumption that the threshold for + // entering discard-after-display mode is at least twice the batch size (since + // that is the most frames-pending-decode we will request) + 1 for the current + // frame. That way the redecoded frames being inserted will never risk + // overlapping the frames we will discard due to the animation progressing. + // That may cause us to use a little more memory than we want but that is an + // acceptable tradeoff for simplicity. + size_t minThreshold = 2 * mBatch + 1; + if (mThreshold < minThreshold) { + mThreshold = minThreshold; + } + + // The maximum number of frames we should ever have decoded at one time is + // twice the batch. That is a good as number as any to start our decoding at. + mPending = mBatch * 2; +} + +bool AnimationFrameRetainedBuffer::InsertInternal(RefPtr&& aFrame) { + // We should only insert new frames if we actually asked for them. + MOZ_ASSERT(!mSizeKnown); + MOZ_ASSERT(mFrames.Length() < mThreshold); + + ++mSize; + mFrames.AppendElement(std::move(aFrame)); + MOZ_ASSERT(mSize == mFrames.Length()); + return mSize < mThreshold; +} + +bool AnimationFrameRetainedBuffer::ResetInternal() { + // If we haven't crossed the threshold, then we know by definition we have + // not discarded any frames. If we previously requested more frames, but + // it would have been more than we would have buffered otherwise, we can + // stop the decoding after one more frame. + if (mPending > 1 && mSize >= mBatch * 2 + 1) { + MOZ_ASSERT(!mSizeKnown); + mPending = 1; + } + + // Either the decoder is still running, or we have enough frames already. + // No need for us to restart it. + return false; +} + +bool AnimationFrameRetainedBuffer::MarkComplete( + const gfx::IntRect& aFirstFrameRefreshArea) { + MOZ_ASSERT(!mSizeKnown); + mFirstFrameRefreshArea = aFirstFrameRefreshArea; + mSizeKnown = true; + mPending = 0; + mFrames.Compact(); + return false; +} + +void AnimationFrameRetainedBuffer::AdvanceInternal() { + // We should not have advanced if we never inserted. + MOZ_ASSERT(!mFrames.IsEmpty()); + // We only want to change the current frame index if we have advanced. This + // means either a higher frame index, or going back to the beginning. + size_t framesLength = mFrames.Length(); + // We should never have advanced beyond the frame buffer. + MOZ_ASSERT(mGetIndex < framesLength); + // We should never advance if the current frame is null -- it needs to know + // the timeout from it at least to know when to advance. + MOZ_ASSERT_IF(mGetIndex > 0, mFrames[mGetIndex - 1]); + MOZ_ASSERT_IF(mGetIndex == 0, mFrames[framesLength - 1]); + // The owner should have already accessed the next frame, so it should also + // be available. + MOZ_ASSERT(mFrames[mGetIndex]); + + if (!mSizeKnown) { + // Calculate how many frames we have requested ahead of the current frame. + size_t buffered = mPending + framesLength - mGetIndex - 1; + if (buffered < mBatch) { + // If we have fewer frames than the batch size, then ask for more. If we + // do not have any pending, then we know that there is no active decoding. + mPending += mBatch; + } + } +} + +imgFrame* AnimationFrameRetainedBuffer::Get(size_t aFrame, bool aForDisplay) { + // We should not have asked for a frame if we never inserted. + if (mFrames.IsEmpty()) { + MOZ_ASSERT_UNREACHABLE("Calling Get() when we have no frames"); + return nullptr; + } + + // If we don't have that frame, return an empty frame ref. + if (aFrame >= mFrames.Length()) { + return nullptr; + } + + // If we have space for the frame, it should always be available. + if (!mFrames[aFrame]) { + MOZ_ASSERT_UNREACHABLE("Calling Get() when frame is unavailable"); + return nullptr; + } + + // If we are advancing on behalf of the animation, we don't expect it to be + // getting any frames (besides the first) until we get the desired frame. + MOZ_ASSERT(aFrame == 0 || mAdvance == 0); + return mFrames[aFrame].get(); +} + +bool AnimationFrameRetainedBuffer::IsFirstFrameFinished() const { + return !mFrames.IsEmpty() && mFrames[0]->IsFinished(); +} + +bool AnimationFrameRetainedBuffer::IsLastInsertedFrame(imgFrame* aFrame) const { + return !mFrames.IsEmpty() && mFrames.LastElement().get() == aFrame; +} + +void AnimationFrameRetainedBuffer::AddSizeOfExcludingThis( + MallocSizeOf aMallocSizeOf, const AddSizeOfCb& aCallback) { + size_t i = 0; + for (const RefPtr& frame : mFrames) { + ++i; + frame->AddSizeOfExcludingThis(aMallocSizeOf, + [&](AddSizeOfCbData& aMetadata) { + aMetadata.mIndex = i; + aCallback(aMetadata); + }); + } +} + +AnimationFrameDiscardingQueue::AnimationFrameDiscardingQueue( + AnimationFrameRetainedBuffer&& aQueue) + : AnimationFrameBuffer(aQueue), + mInsertIndex(aQueue.mFrames.Length()), + mFirstFrame(aQueue.mFrames[0]) { + MOZ_ASSERT(!mSizeKnown); + MOZ_ASSERT(!mRedecodeError); + MOZ_ASSERT(mInsertIndex > 0); + mMayDiscard = true; + + // We avoided moving aQueue.mFrames[0] for mFirstFrame above because it is + // possible the animation was reset back to the beginning, and then we crossed + // the threshold without advancing further. That would mean mGetIndex is 0. + for (size_t i = mGetIndex; i < mInsertIndex; ++i) { + MOZ_ASSERT(aQueue.mFrames[i]); + mDisplay.push_back(std::move(aQueue.mFrames[i])); + } +} + +bool AnimationFrameDiscardingQueue::InsertInternal(RefPtr&& aFrame) { + if (mInsertIndex == mSize) { + if (mSizeKnown) { + // We produced more frames on a subsequent decode than on the first pass. + mRedecodeError = true; + mPending = 0; + return true; + } + ++mSize; + } + + // Even though we don't use redecoded first frames for display purposes, we + // will still use them for recycling, so we still need to insert it. + mDisplay.push_back(std::move(aFrame)); + ++mInsertIndex; + MOZ_ASSERT(mInsertIndex <= mSize); + return true; +} + +bool AnimationFrameDiscardingQueue::ResetInternal() { + mDisplay.clear(); + mInsertIndex = 0; + + bool restartDecoder = mPending == 0; + mPending = 2 * mBatch; + return restartDecoder; +} + +bool AnimationFrameDiscardingQueue::MarkComplete( + const gfx::IntRect& aFirstFrameRefreshArea) { + if (NS_WARN_IF(mInsertIndex != mSize)) { + mRedecodeError = true; + mPending = 0; + } + + // If we encounter a redecode error, just make the first frame refresh area to + // be the full frame, because we don't really know what we can safely recycle. + mFirstFrameRefreshArea = + mRedecodeError ? mFirstFrame->GetRect() : aFirstFrameRefreshArea; + + // We reached the end of the animation, the next frame we get, if we get + // another, will be the first frame again. + mInsertIndex = 0; + mSizeKnown = true; + + // Since we only request advancing when we want to resume at a certain point + // in the animation, we should never exceed the number of frames. + MOZ_ASSERT(mAdvance == 0); + return mPending > 0; +} + +void AnimationFrameDiscardingQueue::AdvanceInternal() { + // We only want to change the current frame index if we have advanced. This + // means either a higher frame index, or going back to the beginning. + // We should never have advanced beyond the frame buffer. + MOZ_ASSERT(mGetIndex < mSize); + + // We should have the current frame still in the display queue. Either way, + // we should at least have an entry in the queue which we need to consume. + MOZ_ASSERT(!mDisplay.empty()); + MOZ_ASSERT(mDisplay.front()); + mDisplay.pop_front(); + MOZ_ASSERT(!mDisplay.empty()); + MOZ_ASSERT(mDisplay.front()); + + if (mDisplay.size() + mPending - 1 < mBatch) { + // If we have fewer frames than the batch size, then ask for more. If we + // do not have any pending, then we know that there is no active decoding. + mPending += mBatch; + } +} + +imgFrame* AnimationFrameDiscardingQueue::Get(size_t aFrame, bool aForDisplay) { + // The first frame is stored separately. If we only need the frame for + // display purposes, we can return it right away. If we need it for advancing + // the animation, we want to verify the recreated first frame is available + // before allowing it continue. + if (aForDisplay && aFrame == 0) { + return mFirstFrame.get(); + } + + // If we don't have that frame, return an empty frame ref. + if (aFrame >= mSize) { + return nullptr; + } + + size_t offset; + if (aFrame >= mGetIndex) { + offset = aFrame - mGetIndex; + } else if (!mSizeKnown) { + MOZ_ASSERT_UNREACHABLE("Requesting previous frame after we have advanced!"); + return nullptr; + } else { + offset = mSize - mGetIndex + aFrame; + } + + if (offset >= mDisplay.size()) { + return nullptr; + } + + // If we are advancing on behalf of the animation, we don't expect it to be + // getting any frames (besides the first) until we get the desired frame. + MOZ_ASSERT(aFrame == 0 || mAdvance == 0); + + // If we have space for the frame, it should always be available. + MOZ_ASSERT(mDisplay[offset]); + return mDisplay[offset].get(); +} + +bool AnimationFrameDiscardingQueue::IsFirstFrameFinished() const { + MOZ_ASSERT(mFirstFrame); + MOZ_ASSERT(mFirstFrame->IsFinished()); + return true; +} + +bool AnimationFrameDiscardingQueue::IsLastInsertedFrame( + imgFrame* aFrame) const { + return !mDisplay.empty() && mDisplay.back().get() == aFrame; +} + +void AnimationFrameDiscardingQueue::AddSizeOfExcludingThis( + MallocSizeOf aMallocSizeOf, const AddSizeOfCb& aCallback) { + mFirstFrame->AddSizeOfExcludingThis(aMallocSizeOf, + [&](AddSizeOfCbData& aMetadata) { + aMetadata.mIndex = 1; + aCallback(aMetadata); + }); + + size_t i = mGetIndex; + for (const RefPtr& frame : mDisplay) { + ++i; + if (mSize < i) { + i = 1; + if (mFirstFrame.get() == frame.get()) { + // First frame again, we already covered it above. We can have a + // different frame in the first frame position in the discard queue + // on subsequent passes of the animation. This is useful for recycling. + continue; + } + } + + frame->AddSizeOfExcludingThis(aMallocSizeOf, + [&](AddSizeOfCbData& aMetadata) { + aMetadata.mIndex = i; + aCallback(aMetadata); + }); + } +} + +AnimationFrameRecyclingQueue::AnimationFrameRecyclingQueue( + AnimationFrameRetainedBuffer&& aQueue) + : AnimationFrameDiscardingQueue(std::move(aQueue)), + mForceUseFirstFrameRefreshArea(false) { + // In an ideal world, we would always save the already displayed frames for + // recycling but none of the frames were marked as recyclable. We will incur + // the extra allocation cost for a few more frames. + mRecycling = true; + + // Until we reach the end of the animation, set the first frame refresh area + // to match that of the full area of the first frame. + mFirstFrameRefreshArea = mFirstFrame->GetRect(); +} + +void AnimationFrameRecyclingQueue::AddSizeOfExcludingThis( + MallocSizeOf aMallocSizeOf, const AddSizeOfCb& aCallback) { + AnimationFrameDiscardingQueue::AddSizeOfExcludingThis(aMallocSizeOf, + aCallback); + + for (const RecycleEntry& entry : mRecycle) { + if (entry.mFrame) { + entry.mFrame->AddSizeOfExcludingThis( + aMallocSizeOf, [&](AddSizeOfCbData& aMetadata) { + aMetadata.mIndex = 0; // Frame is not applicable + aCallback(aMetadata); + }); + } + } +} + +void AnimationFrameRecyclingQueue::AdvanceInternal() { + // We only want to change the current frame index if we have advanced. This + // means either a higher frame index, or going back to the beginning. + // We should never have advanced beyond the frame buffer. + MOZ_ASSERT(mGetIndex < mSize); + + MOZ_ASSERT(!mDisplay.empty()); + MOZ_ASSERT(mDisplay.front()); + + // We have advanced past the first frame. That means the next frame we are + // putting in the queue to recycling is the first frame in the animation, + // and we no longer need to worry about having looped around. + if (mGetIndex == 1) { + mForceUseFirstFrameRefreshArea = false; + } + + RefPtr& front = mDisplay.front(); + RecycleEntry newEntry(mForceUseFirstFrameRefreshArea ? mFirstFrameRefreshArea + : front->GetDirtyRect()); + + // If we are allowed to recycle the frame, then we should save it before the + // base class's AdvanceInternal discards it. + newEntry.mFrame = std::move(front); + + // Even if the frame itself isn't saved, we want the dirty rect to calculate + // the recycle rect for future recycled frames. + mRecycle.push_back(std::move(newEntry)); + mDisplay.pop_front(); + MOZ_ASSERT(!mDisplay.empty()); + MOZ_ASSERT(mDisplay.front()); + + if (mDisplay.size() + mPending - 1 < mBatch) { + // If we have fewer frames than the batch size, then ask for more. If we + // do not have any pending, then we know that there is no active decoding. + // + // We limit the batch to avoid using the frame we just added to the queue. + // This gives other parts of the system time to switch to the new current + // frame, and maximize buffer reuse. In particular this is useful for + // WebRender which holds onto the previous frame for much longer. + size_t newPending = std::min(mPending + mBatch, mRecycle.size() - 1); + if (newPending == 0 && (mDisplay.size() <= 1 || mPending > 0)) { + // If we already have pending frames, then the decoder is active and we + // cannot go below one. If we are displaying the only frame we have, and + // there are none pending, then we must request at least one more frame to + // continue to animation, because we won't advance again without a new + // frame. This may cause us to skip recycling because the previous frame + // is still in use. + newPending = 1; + } + mPending = newPending; + } +} + +bool AnimationFrameRecyclingQueue::ResetInternal() { + // We should save any display frames that we can to save on at least the + // allocation. The first frame refresh area is guaranteed to be the aggregate + // dirty rect or the entire frame, and so the bare minimum area we can + // recycle. We don't need to worry about updating the dirty rect for the + // existing mRecycle entries, because that will happen in RecycleFrame when + // we try to pull out a frame to redecode the first frame. + for (RefPtr& frame : mDisplay) { + RecycleEntry newEntry(mFirstFrameRefreshArea); + newEntry.mFrame = std::move(frame); + mRecycle.push_back(std::move(newEntry)); + } + + return AnimationFrameDiscardingQueue::ResetInternal(); +} + +RawAccessFrameRef AnimationFrameRecyclingQueue::RecycleFrame( + gfx::IntRect& aRecycleRect) { + if (mInsertIndex == 0) { + // If we are recreating the first frame, then we actually have already + // precomputed aggregate of the dirty rects as the first frame refresh + // area. We know that all of the frames still in the recycling queue + // need to take into account the same dirty rect because they are also + // frames which cross the boundary. + // + // Note that this may actually shrink the dirty rect if we estimated it + // earlier with the full frame size and now we have the actual, more + // conservative aggregate for the animation. + for (RecycleEntry& entry : mRecycle) { + entry.mDirtyRect = mFirstFrameRefreshArea; + } + // Until we advance to the first frame again, any subsequent recycled + // frames should also use the first frame refresh area. + mForceUseFirstFrameRefreshArea = true; + } + + if (mRecycle.empty()) { + return RawAccessFrameRef(); + } + + RawAccessFrameRef recycledFrame; + if (mRecycle.front().mFrame) { + recycledFrame = mRecycle.front().mFrame->RawAccessRef(); + MOZ_ASSERT(recycledFrame); + mRecycle.pop_front(); + + if (mForceUseFirstFrameRefreshArea) { + // We are still crossing the loop boundary and cannot rely upon the dirty + // rects of entries in mDisplay to be representative. E.g. The first frame + // is probably has a full frame dirty rect. + aRecycleRect = mFirstFrameRefreshArea; + } else { + // Calculate the recycle rect for the recycled frame. This is the + // cumulative dirty rect of all of the frames ahead of us to be displayed, + // and to be used for recycling. Or in other words, the dirty rect between + // the recycled frame and the decoded frame which reuses the buffer. + // + // We know at this point that mRecycle contains either frames from the end + // of the animation with the first frame refresh area as the dirty rect + // (plus the first frame likewise) and frames with their actual dirty rect + // from the start. mDisplay should also only contain frames from the start + // of the animation onwards. + aRecycleRect.SetRect(0, 0, 0, 0); + for (const RefPtr& frame : mDisplay) { + aRecycleRect = aRecycleRect.Union(frame->GetDirtyRect()); + } + for (const RecycleEntry& entry : mRecycle) { + aRecycleRect = aRecycleRect.Union(entry.mDirtyRect); + } + } + } else { + mRecycle.pop_front(); + } + + return recycledFrame; +} + +} // namespace image +} // namespace mozilla diff --git a/image/AnimationFrameBuffer.h b/image/AnimationFrameBuffer.h new file mode 100644 index 0000000000..b812fe4630 --- /dev/null +++ b/image/AnimationFrameBuffer.h @@ -0,0 +1,480 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_AnimationFrameBuffer_h +#define mozilla_image_AnimationFrameBuffer_h + +#include "ISurfaceProvider.h" +#include + +namespace mozilla { +namespace image { + +/** + * An AnimationFrameBuffer owns the frames outputted by an animated image + * decoder as well as directing its owner on how to drive the decoder, + * whether to produce more or to stop. + * + * This should be subclassed by the different types of queues, depending on + * what behaviour is desired. + */ +class AnimationFrameBuffer { + public: + enum class InsertStatus : uint8_t { + YIELD, // No more frames required at this time. + CONTINUE, // Continue decoding more frames. + DISCARD_YIELD, // Crossed threshold, switch to discarding structure + // and stop decoding more frames. + DISCARD_CONTINUE // Crossed threshold, switch to discarding structure + // and continue decoding more frames. + }; + + /** + * @param aBatch Number of frames we request to be decoded each time it + * decides we need more. + * + * @param aStartFrame The starting frame for the animation. The frame buffer + * will auto-advance (and thus keep the decoding pipeline + * going) until it has reached this frame. Useful when the + * animation was progressing, but the surface was + * discarded, and we had to redecode. + */ + AnimationFrameBuffer(size_t aBatch, size_t aStartFrame) + : mSize(0), + mBatch(aBatch), + mGetIndex(0), + mAdvance(aStartFrame), + mPending(0), + mSizeKnown(false), + mMayDiscard(false), + mRedecodeError(false), + mRecycling(false) { + if (mBatch > SIZE_MAX / 4) { + // Batch size is so big, we will just end up decoding the whole animation. + mBatch = SIZE_MAX / 4; + } else if (mBatch < 1) { + // Never permit a batch size smaller than 1. We always want to be asking + // for at least one frame to start. + mBatch = 1; + } + } + + AnimationFrameBuffer(const AnimationFrameBuffer& aOther) + : mSize(aOther.mSize), + mBatch(aOther.mBatch), + mGetIndex(aOther.mGetIndex), + mAdvance(aOther.mAdvance), + mPending(aOther.mPending), + mSizeKnown(aOther.mSizeKnown), + mMayDiscard(aOther.mMayDiscard), + mRedecodeError(aOther.mRedecodeError), + mRecycling(aOther.mRecycling) {} + + virtual ~AnimationFrameBuffer() {} + + /** + * @returns True if frames post-advance may be discarded and redecoded on + * demand, else false. + */ + bool MayDiscard() const { return mMayDiscard; } + + /** + * @returns True if frames post-advance may be reused after displaying, else + * false. Implies MayDiscard(). + */ + bool IsRecycling() const { + MOZ_ASSERT_IF(mRecycling, mMayDiscard); + return mRecycling; + } + + /** + * @returns True if the frame buffer was ever marked as complete. This implies + * that the total number of frames is known and may be gotten from + * Frames().Length(). + */ + bool SizeKnown() const { return mSizeKnown; } + + /** + * @returns The total number of frames in the animation. If SizeKnown() is + * true, then this is a constant, else it is just the total number of + * frames we have decoded thus far. + */ + size_t Size() const { return mSize; } + + /** + * @returns The first frame refresh area. This is used instead of the dirty + * rect for the last frame when transitioning back to the first frame. + */ + const gfx::IntRect& FirstFrameRefreshArea() const { + return mFirstFrameRefreshArea; + } + + /** + * @returns True if encountered an error during redecode which should cause + * the caller to stop inserting frames. + */ + bool HasRedecodeError() const { return mRedecodeError; } + + /** + * @returns The current frame index we have advanced to. + */ + size_t Displayed() const { return mGetIndex; } + + /** + * @returns Outstanding frames desired from the decoder. + */ + size_t PendingDecode() const { return mPending; } + + /** + * @returns Outstanding frames to advance internally. + */ + size_t PendingAdvance() const { return mAdvance; } + + /** + * @returns Number of frames we request to be decoded each time it decides we + * need more. + */ + size_t Batch() const { return mBatch; } + + /** + * Resets the currently displayed frame of the frame buffer to the beginning. + * + * @returns True if the caller should restart the decoder. + */ + bool Reset() { + mGetIndex = 0; + mAdvance = 0; + return ResetInternal(); + } + + /** + * Advance the currently displayed frame of the frame buffer. If it reaches + * the end, it will loop back to the beginning. It should not be called unless + * a call to Get has returned a valid frame for the next frame index. + * + * As we advance, the number of frames we have buffered ahead of the current + * will shrink. Once that becomes too few, we will request a batch-sized set + * of frames to be decoded from the decoder. + * + * @param aExpectedFrame The frame we expect to have advanced to. This is + * used for confirmation purposes (e.g. asserts). + * + * @returns True if the caller should restart the decoder. + */ + bool AdvanceTo(size_t aExpectedFrame) { + MOZ_ASSERT(mAdvance == 0); + + if (++mGetIndex == mSize && mSizeKnown) { + mGetIndex = 0; + } + MOZ_ASSERT(mGetIndex == aExpectedFrame); + + bool hasPending = mPending > 0; + AdvanceInternal(); + // Restart the decoder if we transitioned from no pending frames being + // decoded, to some pending frames to be decoded. + return !hasPending && mPending > 0; + } + + /** + * Inserts a frame into the frame buffer. + * + * Once we have a sufficient number of frames buffered relative to the + * currently displayed frame, it will return YIELD to indicate the caller + * should stop decoding. Otherwise it will return CONTINUE. + * + * If we cross the threshold, it will return DISCARD_YIELD or DISCARD_CONTINUE + * to indicate that the caller should switch to a new queue type. + * + * @param aFrame The frame to insert into the buffer. + * + * @returns True if the decoder should decode another frame. + */ + InsertStatus Insert(RefPtr&& aFrame) { + MOZ_ASSERT(mPending > 0); + MOZ_ASSERT(aFrame); + + --mPending; + bool retain = InsertInternal(std::move(aFrame)); + + if (mAdvance > 0 && mSize > 1) { + --mAdvance; + ++mGetIndex; + AdvanceInternal(); + } + + if (!retain) { + return mPending > 0 ? InsertStatus::DISCARD_CONTINUE + : InsertStatus::DISCARD_YIELD; + } + + return mPending > 0 ? InsertStatus::CONTINUE : InsertStatus::YIELD; + } + + /** + * Access a specific frame from the frame buffer. It should generally access + * frames in sequential order, increasing in tandem with AdvanceTo calls. The + * first frame may be accessed at any time. The access order should start with + * the same value as that given in Initialize (aStartFrame). + * + * @param aFrame The frame index to access. + * + * @returns The frame, if available. + */ + virtual imgFrame* Get(size_t aFrame, bool aForDisplay) = 0; + + /** + * @returns True if the first frame of the animation (not of the queue) is + * available/finished, else false. + */ + virtual bool IsFirstFrameFinished() const = 0; + + /** + * @returns True if the last inserted frame matches the given frame, else + * false. + */ + virtual bool IsLastInsertedFrame(imgFrame* aFrame) const = 0; + + /** + * This should be called after the last frame has been inserted. If the buffer + * is discarding old frames, it may request more frames to be decoded. In this + * case that means the decoder should start again from the beginning. This + * return value should be used in preference to that of the Insert call. + * + * @returns True if the decoder should decode another frame. + */ + virtual bool MarkComplete(const gfx::IntRect& aFirstFrameRefreshArea) = 0; + + typedef ISurfaceProvider::AddSizeOfCbData AddSizeOfCbData; + typedef ISurfaceProvider::AddSizeOfCb AddSizeOfCb; + + /** + * Accumulate the total cost of all the frames in the buffer. + */ + virtual void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) = 0; + + /** + * Request a recycled frame buffer, and if available, set aRecycleRect to be + * the dirty rect between the contents of the recycled frame, and the restore + * frame (e.g. what we composite on top of) for the next frame to be created. + * + * @returns The frame to be recycled, if available. + */ + virtual RawAccessFrameRef RecycleFrame(gfx::IntRect& aRecycleRect) { + MOZ_ASSERT(!mRecycling); + return RawAccessFrameRef(); + } + + protected: + /** + * Perform the actual insertion of the given frame into the underlying buffer + * representation. mGetIndex shall be the index of the frame we are inserting, + * and mSize and mPending have already been adjusted as needed. + * + * @returns True if the caller should continue as normal, false if the discard + * threshold was crossed and we should change queue types. + */ + virtual bool InsertInternal(RefPtr&& aFrame) = 0; + + /** + * Advance from the current frame to the immediately adjacent next frame. + * mGetIndex shall be the the index of the new current frame after advancing. + * mPending may be adjusted to request more frames. + */ + virtual void AdvanceInternal() = 0; + + /** + * Discard any frames as necessary for the reset. mPending may be adjusted to + * request more frames. + * + * @returns True if the caller should resume decoding new frames, else false. + */ + virtual bool ResetInternal() = 0; + + /// The first frame refresh area. This is used instead of the dirty rect for + /// the last frame when transitioning back to the first frame. + gfx::IntRect mFirstFrameRefreshArea; + + // The total number of frames in the animation. If mSizeKnown is true, it is + // the actual total regardless of how many frames are available, otherwise it + // is the total number of inserted frames. + size_t mSize; + + // The minimum number of frames that we want buffered ahead of the display. + size_t mBatch; + + // The sequential index of the frame we have advanced to. + size_t mGetIndex; + + // The number of frames we need to auto-advance to synchronize with the + // caller. + size_t mAdvance; + + // The number of frames to decode before we stop. + size_t mPending; + + // True if the total number of frames for the animation is known. + bool mSizeKnown; + + // True if this buffer may discard frames. + bool mMayDiscard; + + // True if we encountered an error while redecoding. + bool mRedecodeError; + + // True if this buffer is recycling frames. + bool mRecycling; +}; + +/** + * An AnimationFrameRetainedBuffer will retain all of the frames inserted into + * it. Once it crosses its maximum number of frames, it will recommend + * conversion to a discarding queue. + */ +class AnimationFrameRetainedBuffer final : public AnimationFrameBuffer { + public: + /** + * @param aThreshold Maximum number of frames that may be stored in the frame + * buffer before it may discard already displayed frames. + * Once exceeded, it will discard the previous frame to the + * current frame whenever Advance is called. It always + * retains the first frame. + * + * @param aBatch See AnimationFrameBuffer::AnimationFrameBuffer. + * + * @param aStartFrame See AnimationFrameBuffer::AnimationFrameBuffer. + */ + AnimationFrameRetainedBuffer(size_t aThreshold, size_t aBatch, + size_t aCurrentFrame); + + /** + * @returns Maximum number of frames before we start discarding previous + * frames post-advance. + */ + size_t Threshold() const { return mThreshold; } + + /** + * @returns The frames of this animation, in order. Each element will always + * contain a valid frame. + */ + const nsTArray>& Frames() const { return mFrames; } + + imgFrame* Get(size_t aFrame, bool aForDisplay) override; + bool IsFirstFrameFinished() const override; + bool IsLastInsertedFrame(imgFrame* aFrame) const override; + bool MarkComplete(const gfx::IntRect& aFirstFrameRefreshArea) override; + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) override; + + private: + friend class AnimationFrameDiscardingQueue; + friend class AnimationFrameRecyclingQueue; + + bool InsertInternal(RefPtr&& aFrame) override; + void AdvanceInternal() override; + bool ResetInternal() override; + + // The frames of this animation, in order. + nsTArray> mFrames; + + // The maximum number of frames we can have before discarding. + size_t mThreshold; +}; + +/** + * An AnimationFrameDiscardingQueue will only retain up to mBatch * 2 frames. + * When the animation advances, it will discard the old current frame. + */ +class AnimationFrameDiscardingQueue : public AnimationFrameBuffer { + public: + explicit AnimationFrameDiscardingQueue(AnimationFrameRetainedBuffer&& aQueue); + + imgFrame* Get(size_t aFrame, bool aForDisplay) final; + bool IsFirstFrameFinished() const final; + bool IsLastInsertedFrame(imgFrame* aFrame) const final; + bool MarkComplete(const gfx::IntRect& aFirstFrameRefreshArea) override; + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) override; + + const std::deque>& Display() const { return mDisplay; } + const imgFrame* FirstFrame() const { return mFirstFrame; } + size_t PendingInsert() const { return mInsertIndex; } + + protected: + bool InsertInternal(RefPtr&& aFrame) override; + void AdvanceInternal() override; + bool ResetInternal() override; + + /// The sequential index of the frame we inserting next. + size_t mInsertIndex; + + /// Queue storing frames to be displayed by the animator. The first frame in + /// the queue is the currently displayed frame. + std::deque> mDisplay; + + /// The first frame which is never discarded, and preferentially reused. + RefPtr mFirstFrame; +}; + +/** + * An AnimationFrameRecyclingQueue will only retain up to mBatch * 2 frames. + * When the animation advances, it will place the old current frame into a + * recycling queue to be reused for a future allocation. This only works for + * animated images where we decoded full sized frames into their own buffers, + * so that the buffers are all identically sized and contain the complete frame + * data. + */ +class AnimationFrameRecyclingQueue final + : public AnimationFrameDiscardingQueue { + public: + explicit AnimationFrameRecyclingQueue(AnimationFrameRetainedBuffer&& aQueue); + + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) override; + + RawAccessFrameRef RecycleFrame(gfx::IntRect& aRecycleRect) override; + + struct RecycleEntry { + explicit RecycleEntry(const gfx::IntRect& aDirtyRect) + : mDirtyRect(aDirtyRect) {} + + RecycleEntry(RecycleEntry&& aOther) + : mFrame(std::move(aOther.mFrame)), mDirtyRect(aOther.mDirtyRect) {} + + RecycleEntry& operator=(RecycleEntry&& aOther) { + mFrame = std::move(aOther.mFrame); + mDirtyRect = aOther.mDirtyRect; + return *this; + } + + RecycleEntry(const RecycleEntry& aOther) = delete; + RecycleEntry& operator=(const RecycleEntry& aOther) = delete; + + RefPtr mFrame; // The frame containing the buffer to recycle. + gfx::IntRect mDirtyRect; // The dirty rect of the frame itself. + }; + + const std::deque& Recycle() const { return mRecycle; } + + protected: + void AdvanceInternal() override; + bool ResetInternal() override; + + /// Queue storing frames to be recycled by the decoder to produce its future + /// frames. May contain up to mBatch frames, where the last frame in the queue + /// is adjacent to the first frame in the mDisplay queue. + std::deque mRecycle; + + /// Force recycled frames to use the first frame refresh area as their dirty + /// rect. This is used when we are recycling frames from the end of an + /// animation to produce frames at the beginning of an animation. + bool mForceUseFirstFrameRefreshArea; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_AnimationFrameBuffer_h diff --git a/image/AnimationParams.h b/image/AnimationParams.h new file mode 100644 index 0000000000..1e20eea04a --- /dev/null +++ b/image/AnimationParams.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_AnimationParams_h +#define mozilla_image_AnimationParams_h + +#include +#include "mozilla/gfx/Rect.h" +#include "FrameTimeout.h" + +namespace mozilla { +namespace image { + +enum class BlendMethod : int8_t { + // All color components of the frame, including alpha, overwrite the current + // contents of the frame's output buffer region. + SOURCE, + + // The frame should be composited onto the output buffer based on its alpha, + // using a simple OVER operation. + OVER +}; + +enum class DisposalMethod : int8_t { + CLEAR_ALL = -1, // Clear the whole image, revealing what's underneath. + NOT_SPECIFIED, // Leave the frame and let the new frame draw on top. + KEEP, // Leave the frame and let the new frame draw on top. + CLEAR, // Clear the frame's area, revealing what's underneath. + RESTORE_PREVIOUS // Restore the previous (composited) frame. +}; + +struct AnimationParams { + AnimationParams(const gfx::IntRect& aBlendRect, const FrameTimeout& aTimeout, + uint32_t aFrameNum, BlendMethod aBlendMethod, + DisposalMethod aDisposalMethod) + : mBlendRect(aBlendRect), + mTimeout(aTimeout), + mFrameNum(aFrameNum), + mBlendMethod(aBlendMethod), + mDisposalMethod(aDisposalMethod) {} + + gfx::IntRect mBlendRect; + FrameTimeout mTimeout; + uint32_t mFrameNum; + BlendMethod mBlendMethod; + DisposalMethod mDisposalMethod; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_AnimationParams_h diff --git a/image/AnimationSurfaceProvider.cpp b/image/AnimationSurfaceProvider.cpp new file mode 100644 index 0000000000..05208a4d19 --- /dev/null +++ b/image/AnimationSurfaceProvider.cpp @@ -0,0 +1,534 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AnimationSurfaceProvider.h" + +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/layers/SharedSurfacesChild.h" +#include "mozilla/layers/SourceSurfaceSharedData.h" +#include "nsProxyRelease.h" + +#include "DecodePool.h" +#include "Decoder.h" + +using namespace mozilla::gfx; +using namespace mozilla::layers; + +namespace mozilla { +namespace image { + +AnimationSurfaceProvider::AnimationSurfaceProvider( + NotNull aImage, const SurfaceKey& aSurfaceKey, + NotNull aDecoder, size_t aCurrentFrame) + : ISurfaceProvider(ImageKey(aImage.get()), aSurfaceKey, + AvailabilityState::StartAsPlaceholder()), + mImage(aImage.get()), + mDecodingMutex("AnimationSurfaceProvider::mDecoder"), + mDecoder(aDecoder.get()), + mFramesMutex("AnimationSurfaceProvider::mFrames"), + mCompositedFrameRequested(false), + mSharedAnimation(MakeRefPtr()) { + MOZ_ASSERT(!mDecoder->IsMetadataDecode(), + "Use MetadataDecodingTask for metadata decodes"); + MOZ_ASSERT(!mDecoder->IsFirstFrameDecode(), + "Use DecodedSurfaceProvider for single-frame image decodes"); + + // Calculate how many frames we need to decode in this animation before we + // enter decode-on-demand mode. + IntSize frameSize = aSurfaceKey.Size(); + size_t threshold = + (size_t(StaticPrefs::image_animated_decode_on_demand_threshold_kb()) * + 1024) / + (sizeof(uint32_t) * frameSize.width * frameSize.height); + size_t batch = StaticPrefs::image_animated_decode_on_demand_batch_size(); + + mFrames.reset( + new AnimationFrameRetainedBuffer(threshold, batch, aCurrentFrame)); +} + +AnimationSurfaceProvider::~AnimationSurfaceProvider() { + DropImageReference(); + + mSharedAnimation->Destroy(); + if (mDecoder) { + mDecoder->SetFrameRecycler(nullptr); + } +} + +void AnimationSurfaceProvider::DropImageReference() { + if (!mImage) { + return; // Nothing to do. + } + + // RasterImage objects need to be destroyed on the main thread. + SurfaceCache::ReleaseImageOnMainThread(mImage.forget()); +} + +void AnimationSurfaceProvider::Reset() { + // We want to go back to the beginning. + bool mayDiscard; + bool restartDecoder = false; + + { + MutexAutoLock lock(mFramesMutex); + + // If we have not crossed the threshold, we know we haven't discarded any + // frames, and thus we know it is safe move our display index back to the + // very beginning. It would be cleaner to let the frame buffer make this + // decision inside the AnimationFrameBuffer::Reset method, but if we have + // crossed the threshold, we need to hold onto the decoding mutex too. We + // should avoid blocking the main thread on the decoder threads. + mayDiscard = mFrames->MayDiscard(); + if (!mayDiscard) { + restartDecoder = mFrames->Reset(); + } + } + + if (mayDiscard) { + // We are over the threshold and have started discarding old frames. In + // that case we need to seize the decoding mutex. Thankfully we know that + // we are in the process of decoding at most the batch size frames, so + // this should not take too long to acquire. + MutexAutoLock lock(mDecodingMutex); + + // We may have hit an error while redecoding. Because FrameAnimator is + // tightly coupled to our own state, that means we would need to go through + // some heroics to resume animating in those cases. The typical reason for + // a redecode to fail is out of memory, and recycling should prevent most of + // those errors. When image.animated.generate-full-frames has shipped + // enabled on a release or two, we can simply remove the old FrameAnimator + // blending code and simplify this quite a bit -- just always pop the next + // full frame and timeout off the stack. + if (mDecoder) { + mDecoder = DecoderFactory::CloneAnimationDecoder(mDecoder); + MOZ_ASSERT(mDecoder); + + MutexAutoLock lock2(mFramesMutex); + restartDecoder = mFrames->Reset(); + } else { + MOZ_ASSERT(mFrames->HasRedecodeError()); + } + } + + if (restartDecoder) { + DecodePool::Singleton()->AsyncRun(this); + } +} + +void AnimationSurfaceProvider::Advance(size_t aFrame) { + bool restartDecoder; + + RefPtr surface; + IntRect dirtyRect; + { + // Typical advancement of a frame. + MutexAutoLock lock(mFramesMutex); + restartDecoder = mFrames->AdvanceTo(aFrame); + + imgFrame* frame = mFrames->Get(aFrame, /* aForDisplay */ true); + MOZ_ASSERT(frame); + if (aFrame != 0) { + dirtyRect = frame->GetDirtyRect(); + } else { + MOZ_ASSERT(mFrames->SizeKnown()); + dirtyRect = mFrames->FirstFrameRefreshArea(); + } + surface = frame->GetSourceSurface(); + MOZ_ASSERT(surface); + } + + if (restartDecoder) { + DecodePool::Singleton()->AsyncRun(this); + } + + mCompositedFrameRequested = false; + auto* sharedSurface = static_cast(surface.get()); + mSharedAnimation->SetCurrentFrame(sharedSurface, dirtyRect); +} + +DrawableFrameRef AnimationSurfaceProvider::DrawableRef(size_t aFrame) { + MutexAutoLock lock(mFramesMutex); + + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling DrawableRef() on a placeholder"); + return DrawableFrameRef(); + } + + imgFrame* frame = mFrames->Get(aFrame, /* aForDisplay */ true); + if (!frame) { + return DrawableFrameRef(); + } + + return frame->DrawableRef(); +} + +already_AddRefed AnimationSurfaceProvider::GetFrame(size_t aFrame) { + MutexAutoLock lock(mFramesMutex); + + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling GetFrame() on a placeholder"); + return nullptr; + } + + RefPtr frame = mFrames->Get(aFrame, /* aForDisplay */ false); + MOZ_ASSERT_IF(frame, frame->IsFinished()); + return frame.forget(); +} + +bool AnimationSurfaceProvider::IsFinished() const { + MutexAutoLock lock(mFramesMutex); + + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling IsFinished() on a placeholder"); + return false; + } + + return mFrames->IsFirstFrameFinished(); +} + +bool AnimationSurfaceProvider::IsFullyDecoded() const { + MutexAutoLock lock(mFramesMutex); + return mFrames->SizeKnown() && !mFrames->MayDiscard(); +} + +size_t AnimationSurfaceProvider::LogicalSizeInBytes() const { + // When decoding animated images, we need at most three live surfaces: the + // composited surface, the previous composited surface for + // DisposalMethod::RESTORE_PREVIOUS, and the surface we're currently decoding + // into. The composited surfaces are always BGRA. Although the surface we're + // decoding into may be paletted, and may be smaller than the real size of the + // image, we assume the worst case here. + // XXX(seth): Note that this is actually not accurate yet; we're storing the + // full sequence of frames, not just the three live surfaces mentioned above. + // Unfortunately there's no way to know in advance how many frames an + // animation has, so we really can't do better here. This will become correct + // once bug 1289954 is complete. + IntSize size = GetSurfaceKey().Size(); + return 3 * size.width * size.height * sizeof(uint32_t); +} + +void AnimationSurfaceProvider::AddSizeOfExcludingThis( + MallocSizeOf aMallocSizeOf, const AddSizeOfCb& aCallback) { + // Note that the surface cache lock is already held here, and then we acquire + // mFramesMutex. For this method, this ordering is unavoidable, which means + // that we must be careful to always use the same ordering elsewhere. + MutexAutoLock lock(mFramesMutex); + mFrames->AddSizeOfExcludingThis(aMallocSizeOf, aCallback); +} + +void AnimationSurfaceProvider::Run() { + MutexAutoLock lock(mDecodingMutex); + + if (!mDecoder) { + MOZ_ASSERT_UNREACHABLE("Running after decoding finished?"); + return; + } + + while (true) { + // Run the decoder. + LexerResult result = mDecoder->Decode(WrapNotNull(this)); + + if (result.is()) { + // We may have a new frame now, but it's not guaranteed - a decoding + // failure or truncated data may mean that no new frame got produced. + // Since we're not sure, rather than call CheckForNewFrameAtYield() here + // we call CheckForNewFrameAtTerminalState(), which handles both of these + // possibilities. + bool continueDecoding = CheckForNewFrameAtTerminalState(); + FinishDecoding(); + + // Even if it is the last frame, we may not have enough frames buffered + // ahead of the current. If we are shutting down, we want to ensure we + // release the thread as soon as possible. The animation may advance even + // during shutdown, which keeps us decoding, and thus blocking the decode + // pool during teardown. + if (!mDecoder || !continueDecoding || + DecodePool::Singleton()->IsShuttingDown()) { + return; + } + + // Restart from the very beginning because the decoder was recreated. + continue; + } + + // If there is output available we want to change the entry in the surface + // cache from a placeholder to an actual surface now before NotifyProgress + // call below so that when consumers get the frame complete notification + // from the NotifyProgress they can actually get a surface from the surface + // cache. + bool checkForNewFrameAtYieldResult = false; + if (result == LexerResult(Yield::OUTPUT_AVAILABLE)) { + checkForNewFrameAtYieldResult = CheckForNewFrameAtYield(); + } + + // Notify for the progress we've made so far. + if (mImage && mDecoder->HasProgress()) { + NotifyProgress(WrapNotNull(mImage), WrapNotNull(mDecoder)); + } + + if (result == LexerResult(Yield::NEED_MORE_DATA)) { + // We can't make any more progress right now. The decoder itself will + // ensure that we get reenqueued when more data is available; just return + // for now. + return; + } + + // There's new output available - a new frame! Grab it. If we don't need any + // more for the moment we can break out of the loop. If we are shutting + // down, we want to ensure we release the thread as soon as possible. The + // animation may advance even during shutdown, which keeps us decoding, and + // thus blocking the decode pool during teardown. + MOZ_ASSERT(result == LexerResult(Yield::OUTPUT_AVAILABLE)); + if (!checkForNewFrameAtYieldResult || + DecodePool::Singleton()->IsShuttingDown()) { + return; + } + } +} + +bool AnimationSurfaceProvider::CheckForNewFrameAtYield() { + mDecodingMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mDecoder); + + bool justGotFirstFrame = false; + bool continueDecoding = false; + + { + MutexAutoLock lock(mFramesMutex); + + // Try to get the new frame from the decoder. + RefPtr frame = mDecoder->GetCurrentFrame(); + MOZ_ASSERT(mDecoder->HasFrameToTake()); + mDecoder->ClearHasFrameToTake(); + + if (!frame) { + MOZ_ASSERT_UNREACHABLE("Decoder yielded but didn't produce a frame?"); + return true; + } + + // We should've gotten a different frame than last time. + MOZ_ASSERT(!mFrames->IsLastInsertedFrame(frame)); + + // Append the new frame to the list. + AnimationFrameBuffer::InsertStatus status = + mFrames->Insert(std::move(frame)); + + // If we hit a redecode error, then we actually want to stop. This happens + // when we tried to insert more frames than we originally had (e.g. the + // original decoder attempt hit an OOM error sooner than we did). Better to + // stop the animation than to get out of sync with FrameAnimator. + if (mFrames->HasRedecodeError()) { + mDecoder = nullptr; + return false; + } + + switch (status) { + case AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE: + continueDecoding = true; + [[fallthrough]]; + case AnimationFrameBuffer::InsertStatus::DISCARD_YIELD: + RequestFrameDiscarding(); + break; + case AnimationFrameBuffer::InsertStatus::CONTINUE: + continueDecoding = true; + break; + case AnimationFrameBuffer::InsertStatus::YIELD: + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled insert status!"); + break; + } + + // We only want to handle the first frame if it is the first pass for the + // animation decoder. The owning image will be cleared after that. + size_t frameCount = mFrames->Size(); + if (frameCount == 1 && mImage) { + justGotFirstFrame = true; + } + } + + if (justGotFirstFrame) { + AnnounceSurfaceAvailable(); + } + + return continueDecoding; +} + +bool AnimationSurfaceProvider::CheckForNewFrameAtTerminalState() { + mDecodingMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mDecoder); + + bool justGotFirstFrame = false; + bool continueDecoding; + + { + MutexAutoLock lock(mFramesMutex); + + // The decoder may or may not have a new frame for us at this point. Avoid + // reinserting the same frame again. + RefPtr frame = mDecoder->GetCurrentFrame(); + + // If the decoder didn't finish a new frame (ie if, after starting the + // frame, it got an error and aborted the frame and the rest of the decode) + // that means it won't be reporting it to the image or FrameAnimator so we + // should ignore it too, that's what HasFrameToTake tracks basically. + if (!mDecoder->HasFrameToTake()) { + frame = nullptr; + } else { + MOZ_ASSERT(frame); + mDecoder->ClearHasFrameToTake(); + } + + if (!frame || mFrames->IsLastInsertedFrame(frame)) { + return mFrames->MarkComplete(mDecoder->GetFirstFrameRefreshArea()); + } + + // Append the new frame to the list. + AnimationFrameBuffer::InsertStatus status = + mFrames->Insert(std::move(frame)); + + // If we hit a redecode error, then we actually want to stop. This will be + // fully handled in FinishDecoding. + if (mFrames->HasRedecodeError()) { + return false; + } + + switch (status) { + case AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE: + case AnimationFrameBuffer::InsertStatus::DISCARD_YIELD: + RequestFrameDiscarding(); + break; + case AnimationFrameBuffer::InsertStatus::CONTINUE: + case AnimationFrameBuffer::InsertStatus::YIELD: + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled insert status!"); + break; + } + + continueDecoding = + mFrames->MarkComplete(mDecoder->GetFirstFrameRefreshArea()); + + // We only want to handle the first frame if it is the first pass for the + // animation decoder. The owning image will be cleared after that. + if (mFrames->Size() == 1 && mImage) { + justGotFirstFrame = true; + } + } + + if (justGotFirstFrame) { + AnnounceSurfaceAvailable(); + } + + return continueDecoding; +} + +void AnimationSurfaceProvider::RequestFrameDiscarding() { + mDecodingMutex.AssertCurrentThreadOwns(); + mFramesMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mDecoder); + + if (mFrames->MayDiscard() || mFrames->IsRecycling()) { + MOZ_ASSERT_UNREACHABLE("Already replaced frame queue!"); + return; + } + + auto oldFrameQueue = + static_cast(mFrames.get()); + + MOZ_ASSERT(!mDecoder->GetFrameRecycler()); + if (StaticPrefs::image_animated_decode_on_demand_recycle_AtStartup()) { + mFrames.reset(new AnimationFrameRecyclingQueue(std::move(*oldFrameQueue))); + mDecoder->SetFrameRecycler(this); + } else { + mFrames.reset(new AnimationFrameDiscardingQueue(std::move(*oldFrameQueue))); + } +} + +void AnimationSurfaceProvider::AnnounceSurfaceAvailable() { + mFramesMutex.AssertNotCurrentThreadOwns(); + MOZ_ASSERT(mImage); + + // We just got the first frame; let the surface cache know. We deliberately do + // this outside of mFramesMutex to avoid a potential deadlock with + // AddSizeOfExcludingThis(), since otherwise we'd be acquiring mFramesMutex + // and then the surface cache lock, while the memory reporting code would + // acquire the surface cache lock and then mFramesMutex. + SurfaceCache::SurfaceAvailable(WrapNotNull(this)); +} + +void AnimationSurfaceProvider::FinishDecoding() { + mDecodingMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mDecoder); + + if (mImage) { + // Send notifications. + NotifyDecodeComplete(WrapNotNull(mImage), WrapNotNull(mDecoder)); + } + + // Determine if we need to recreate the decoder, in case we are discarding + // frames and need to loop back to the beginning. + bool recreateDecoder; + { + MutexAutoLock lock(mFramesMutex); + recreateDecoder = !mFrames->HasRedecodeError() && mFrames->MayDiscard(); + } + + if (recreateDecoder) { + mDecoder = DecoderFactory::CloneAnimationDecoder(mDecoder); + MOZ_ASSERT(mDecoder); + } else { + mDecoder = nullptr; + } + + // We don't need a reference to our image anymore, either, and we don't want + // one. We may be stored in the surface cache for a long time after decoding + // finishes. If we don't drop our reference to the image, we'll end up + // keeping it alive as long as we remain in the surface cache, which could + // greatly extend the image's lifetime - in fact, if the image isn't + // discardable, it'd result in a leak! + DropImageReference(); +} + +bool AnimationSurfaceProvider::ShouldPreferSyncRun() const { + MutexAutoLock lock(mDecodingMutex); + MOZ_ASSERT(mDecoder); + + return mDecoder->ShouldSyncDecode( + StaticPrefs::image_mem_decode_bytes_at_a_time_AtStartup()); +} + +RawAccessFrameRef AnimationSurfaceProvider::RecycleFrame( + gfx::IntRect& aRecycleRect) { + MutexAutoLock lock(mFramesMutex); + MOZ_ASSERT(mFrames->IsRecycling()); + return mFrames->RecycleFrame(aRecycleRect); +} + +nsresult AnimationSurfaceProvider::UpdateKey( + layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, wr::ImageKey& aKey) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr surface; + { + MutexAutoLock lock(mFramesMutex); + imgFrame* frame = + mFrames->Get(mFrames->Displayed(), /* aForDisplay */ true); + if (!frame) { + return NS_ERROR_NOT_AVAILABLE; + } + + surface = frame->GetSourceSurface(); + } + + mCompositedFrameRequested = true; + auto* sharedSurface = static_cast(surface.get()); + return mSharedAnimation->UpdateKey(sharedSurface, aManager, aResources, aKey); +} + +} // namespace image +} // namespace mozilla diff --git a/image/AnimationSurfaceProvider.h b/image/AnimationSurfaceProvider.h new file mode 100644 index 0000000000..920638279e --- /dev/null +++ b/image/AnimationSurfaceProvider.h @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * An ISurfaceProvider for animated images. + */ + +#ifndef mozilla_image_AnimationSurfaceProvider_h +#define mozilla_image_AnimationSurfaceProvider_h + +#include "mozilla/UniquePtr.h" + +#include "Decoder.h" +#include "FrameAnimator.h" +#include "IDecodingTask.h" +#include "ISurfaceProvider.h" +#include "AnimationFrameBuffer.h" + +namespace mozilla { +namespace layers { +class SharedSurfacesAnimation; +} + +namespace image { + +/** + * An ISurfaceProvider that manages the decoding of animated images and + * dynamically generates surfaces for the current playback state of the + * animation. + */ +class AnimationSurfaceProvider final : public ISurfaceProvider, + public IDecodingTask, + public IDecoderFrameRecycler { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AnimationSurfaceProvider, override) + + AnimationSurfaceProvider(NotNull aImage, + const SurfaceKey& aSurfaceKey, + NotNull aDecoder, size_t aCurrentFrame); + + ////////////////////////////////////////////////////////////////////////////// + // ISurfaceProvider implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + bool IsFinished() const override; + bool IsFullyDecoded() const override; + size_t LogicalSizeInBytes() const override; + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) override; + void Reset() override; + void Advance(size_t aFrame) override; + bool MayAdvance() const override { return mCompositedFrameRequested; } + void MarkMayAdvance() override { mCompositedFrameRequested = true; } + + protected: + DrawableFrameRef DrawableRef(size_t aFrame) override; + already_AddRefed GetFrame(size_t aFrame) override; + + // Animation frames are always locked. This is because we only want to release + // their memory atomically (due to the surface cache discarding them). If they + // were unlocked, the OS could end up releasing the memory of random frames + // from the middle of the animation, which is not worth the complexity of + // dealing with. + bool IsLocked() const override { return true; } + void SetLocked(bool) override {} + + ////////////////////////////////////////////////////////////////////////////// + // IDecodingTask implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + void Run() override; + bool ShouldPreferSyncRun() const override; + + // Full decodes are low priority compared to metadata decodes because they + // don't block layout or page load. + TaskPriority Priority() const override { return TaskPriority::eLow; } + + ////////////////////////////////////////////////////////////////////////////// + // IDecoderFrameRecycler implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + RawAccessFrameRef RecycleFrame(gfx::IntRect& aRecycleRect) override; + + ////////////////////////////////////////////////////////////////////////////// + // IDecoderFrameRecycler implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + nsresult UpdateKey(layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + wr::ImageKey& aKey) override; + + private: + virtual ~AnimationSurfaceProvider(); + + void DropImageReference(); + void AnnounceSurfaceAvailable(); + void FinishDecoding(); + void RequestFrameDiscarding(); + + // @returns Whether or not we should continue decoding. + bool CheckForNewFrameAtYield(); + + // @returns Whether or not we should restart decoding. + bool CheckForNewFrameAtTerminalState(); + + /// The image associated with our decoder. + RefPtr mImage; + + /// A mutex to protect mDecoder. Always taken before mFramesMutex. + mutable Mutex mDecodingMutex MOZ_UNANNOTATED; + + /// The decoder used to decode this animation. + RefPtr mDecoder; + + /// A mutex to protect mFrames. Always taken after mDecodingMutex. + mutable Mutex mFramesMutex MOZ_UNANNOTATED; + + /// The frames of this animation, in order. + UniquePtr mFrames; + + /// Whether the current frame was requested for display since the last time we + /// advanced the animation. + bool mCompositedFrameRequested; + + /// + RefPtr mSharedAnimation; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_AnimationSurfaceProvider_h diff --git a/image/AutoRestoreSVGState.h b/image/AutoRestoreSVGState.h new file mode 100644 index 0000000000..c01c2f612b --- /dev/null +++ b/image/AutoRestoreSVGState.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_AutoRestoreSVGState_h +#define mozilla_image_AutoRestoreSVGState_h + +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/SVGContextPaint.h" +#include "mozilla/dom/SVGSVGElement.h" +#include "nsPresContext.h" +#include "SVGDrawingParameters.h" +#include "SVGDocumentWrapper.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/SVGDocument.h" +#include "mozilla/dom/BrowsingContextBinding.h" + +namespace mozilla::image { + +class MOZ_STACK_CLASS AutoRestoreSVGState final { + public: + AutoRestoreSVGState(const SVGDrawingParameters& aParams, + SVGDocumentWrapper* aSVGDocumentWrapper, + bool aContextPaint) + : AutoRestoreSVGState(aParams.svgContext, aParams.animationTime, + aSVGDocumentWrapper, aContextPaint) {} + + AutoRestoreSVGState(const SVGImageContext& aSVGContext, float aAnimationTime, + SVGDocumentWrapper* aSVGDocumentWrapper, + bool aContextPaint) + : mIsDrawing(aSVGDocumentWrapper->mIsDrawing), + // Apply any 'preserveAspectRatio' override (if specified) to the root + // element: + mPAR(aSVGContext, aSVGDocumentWrapper->GetRootSVGElem()), + // Set the animation time: + mTime(aSVGDocumentWrapper->GetRootSVGElem(), aAnimationTime) { + MOZ_ASSERT(!mIsDrawing.SavedValue()); + MOZ_ASSERT(aSVGDocumentWrapper->GetDocument()); + + if (auto* pc = aSVGDocumentWrapper->GetDocument()->GetPresContext()) { + pc->SetColorSchemeOverride([&] { + if (auto scheme = aSVGContext.GetColorScheme()) { + return *scheme == ColorScheme::Light + ? dom::PrefersColorSchemeOverride::Light + : dom::PrefersColorSchemeOverride::Dark; + } + return dom::PrefersColorSchemeOverride::None; + }()); + } + + aSVGDocumentWrapper->mIsDrawing = true; + + // Set context paint (if specified) on the document: + if (aContextPaint) { + MOZ_ASSERT(aSVGContext.GetContextPaint()); + mContextPaint.emplace(aSVGContext.GetContextPaint(), + aSVGDocumentWrapper->GetDocument()); + } + } + + private: + AutoRestore mIsDrawing; + AutoPreserveAspectRatioOverride mPAR; + AutoSVGTimeSetRestore mTime; + Maybe mContextPaint; +}; + +} // namespace mozilla::image + +#endif // mozilla_image_AutoRestoreSVGState_h diff --git a/image/BMPHeaders.h b/image/BMPHeaders.h new file mode 100644 index 0000000000..2b58f6054c --- /dev/null +++ b/image/BMPHeaders.h @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_BMPHeaders_h +#define mozilla_image_BMPHeaders_h + +#include +#include + +namespace mozilla { +namespace image { +namespace bmp { + +// The length of the file header as defined in the BMP spec. +static const size_t FILE_HEADER_LENGTH = 14; + +// This lengths of the info header for the different BMP versions. +struct InfoHeaderLength { + enum { + WIN_V2 = 12, + WIN_V3 = 40, + WIN_V4 = 108, + WIN_V5 = 124, + + // OS2_V1 is omitted; it's the same as WIN_V2. + OS2_V2_MIN = 16, // Minimum allowed value for OS2v2. + OS2_V2_MAX = 64, // Maximum allowed value for OS2v2. + + WIN_ICO = WIN_V3, + }; +}; + +enum class InfoColorSpace : uint32_t { + CALIBRATED_RGB = 0x00000000, + SRGB = 0x73524742, + WIN = 0x57696E20, + LINKED = 0x4C494E4B, + EMBEDDED = 0x4D424544, +}; + +enum class InfoColorIntent : uint32_t { + BUSINESS = 0x00000001, + GRAPHICS = 0x00000002, + IMAGES = 0x00000004, + ABS_COLORIMETRIC = 0x00000008, +}; + +} // namespace bmp +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_BMPHeaders_h diff --git a/image/BlobSurfaceProvider.cpp b/image/BlobSurfaceProvider.cpp new file mode 100644 index 0000000000..1e9e8dacca --- /dev/null +++ b/image/BlobSurfaceProvider.cpp @@ -0,0 +1,310 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BlobSurfaceProvider.h" +#include "AutoRestoreSVGState.h" +#include "ImageRegion.h" +#include "SVGDocumentWrapper.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/layers/IpcResourceUpdateQueue.h" +#include "mozilla/layers/WebRenderBridgeChild.h" +#include "mozilla/layers/WebRenderDrawEventRecorder.h" + +using namespace mozilla::gfx; +using namespace mozilla::layers; + +namespace mozilla::image { + +BlobSurfaceProvider::BlobSurfaceProvider( + const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + image::SVGDocumentWrapper* aSVGDocumentWrapper, uint32_t aImageFlags) + : ISurfaceProvider(aImageKey, aSurfaceKey, + AvailabilityState::StartAvailable()), + mSVGDocumentWrapper(aSVGDocumentWrapper), + mImageFlags(aImageFlags) { + MOZ_ASSERT(mSVGDocumentWrapper); + MOZ_ASSERT(aImageFlags & imgIContainer::FLAG_RECORD_BLOB); +} + +BlobSurfaceProvider::~BlobSurfaceProvider() { + if (NS_IsMainThread()) { + DestroyKeys(mKeys); + return; + } + + NS_ReleaseOnMainThread("SourceSurfaceBlobImage::mSVGDocumentWrapper", + mSVGDocumentWrapper.forget()); + NS_DispatchToMainThread( + NS_NewRunnableFunction("SourceSurfaceBlobImage::DestroyKeys", + [keys = std::move(mKeys)] { DestroyKeys(keys); })); +} + +/* static */ void BlobSurfaceProvider::DestroyKeys( + const AutoTArray& aKeys) { + for (const auto& entry : aKeys) { + if (entry.mManager->IsDestroyed()) { + continue; + } + + WebRenderBridgeChild* wrBridge = entry.mManager->WrBridge(); + if (!wrBridge || !wrBridge->MatchesNamespace(entry.mBlobKey)) { + continue; + } + + entry.mManager->GetRenderRootStateManager()->AddBlobImageKeyForDiscard( + entry.mBlobKey); + } +} + +nsresult BlobSurfaceProvider::UpdateKey( + layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, wr::ImageKey& aKey) { + MOZ_ASSERT(NS_IsMainThread()); + + layers::WebRenderLayerManager* manager = aManager->LayerManager(); + MOZ_ASSERT(manager); + + Maybe key; + auto i = mKeys.Length(); + while (i > 0) { + --i; + BlobImageKeyData& entry = mKeys[i]; + if (entry.mManager->IsDestroyed()) { + mKeys.RemoveElementAt(i); + } else if (entry.mManager == manager) { + WebRenderBridgeChild* wrBridge = manager->WrBridge(); + MOZ_ASSERT(wrBridge); + + bool ownsKey = wrBridge->MatchesNamespace(entry.mBlobKey); + if (ownsKey && !entry.mDirty) { + key.emplace(entry.mBlobKey); + continue; + } + + // Even if the manager is the same, its underlying WebRenderBridgeChild + // can change state. Either our namespace differs, and our old key has + // already been discarded, or the blob has changed. Either way, we need + // to rerecord it. + auto newEntry = RecordDrawing(manager, aResources, + ownsKey ? Some(entry.mBlobKey) : Nothing()); + if (!newEntry) { + if (ownsKey) { + aManager->AddBlobImageKeyForDiscard(entry.mBlobKey); + } + mKeys.RemoveElementAt(i); + continue; + } + + key.emplace(newEntry.ref().mBlobKey); + entry = std::move(newEntry.ref()); + MOZ_ASSERT(!entry.mDirty); + } + } + + // We didn't find an entry. Attempt to record the blob with a new key. + if (!key) { + auto newEntry = RecordDrawing(manager, aResources, Nothing()); + if (newEntry) { + key.emplace(newEntry.ref().mBlobKey); + mKeys.AppendElement(std::move(newEntry.ref())); + } + } + + if (key) { + aKey = wr::AsImageKey(key.value()); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +void BlobSurfaceProvider::InvalidateRecording() { + MOZ_ASSERT(NS_IsMainThread()); + + auto i = mKeys.Length(); + while (i > 0) { + --i; + BlobImageKeyData& entry = mKeys[i]; + if (entry.mManager->IsDestroyed()) { + mKeys.RemoveElementAt(i); + } else { + entry.mDirty = true; + } + } +} + +Maybe BlobSurfaceProvider::RecordDrawing( + WebRenderLayerManager* aManager, wr::IpcResourceUpdateQueue& aResources, + Maybe aBlobKey) { + MOZ_ASSERT(!aManager->IsDestroyed()); + + if (mSVGDocumentWrapper->IsDrawing()) { + return Nothing(); + } + + // This is either our first pass, or we have a stale key requiring us to + // re-record the SVG image draw commands. + auto* rootManager = aManager->GetRenderRootStateManager(); + auto* wrBridge = aManager->WrBridge(); + + const auto& size = GetSurfaceKey().Size(); + const auto& region = GetSurfaceKey().Region(); + const auto& svgContext = GetSurfaceKey().SVGContext(); + + IntRect imageRect = region ? region->Rect() : IntRect(IntPoint(0, 0), size); + IntRect imageRectOrigin = imageRect - imageRect.TopLeft(); + + std::vector> fonts; + bool validFonts = true; + RefPtr recorder = + MakeAndAddRef( + [&](MemStream& aStream, + std::vector>& aScaledFonts) { + auto count = aScaledFonts.size(); + aStream.write((const char*)&count, sizeof(count)); + + for (auto& scaled : aScaledFonts) { + Maybe key = + wrBridge->GetFontKeyForScaledFont(scaled, aResources); + if (key.isNothing()) { + validFonts = false; + break; + } + BlobFont font = {key.value(), scaled}; + aStream.write((const char*)&font, sizeof(font)); + } + + fonts = std::move(aScaledFonts); + }); + + RefPtr dummyDt = + gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + RefPtr dt = + Factory::CreateRecordingDrawTarget(recorder, dummyDt, imageRectOrigin); + + if (!dt || !dt->IsValid()) { + return Nothing(); + } + + bool contextPaint = svgContext.GetContextPaint(); + + float animTime = (GetSurfaceKey().Playback() == PlaybackType::eStatic) + ? 0.0f + : mSVGDocumentWrapper->GetCurrentTimeAsFloat(); + + IntSize viewportSize = size; + if (auto cssViewportSize = svgContext.GetViewportSize()) { + // XXX losing unit + viewportSize.SizeTo(cssViewportSize->width, cssViewportSize->height); + } + + { + // Get (& sanity-check) the helper-doc's presShell + RefPtr presShell = mSVGDocumentWrapper->GetPresShell(); + MOZ_ASSERT(presShell, "GetPresShell returned null for an SVG image?"); + + nsPresContext* presContext = presShell->GetPresContext(); + MOZ_ASSERT(presContext, "pres shell w/out pres context"); + + auto* doc = presShell->GetDocument(); + [[maybe_unused]] nsIURI* uri = doc ? doc->GetDocumentURI() : nullptr; + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING( + "SVG Image recording", GRAPHICS, + nsPrintfCString("(%d,%d) %dx%d from %dx%d %s", imageRect.x, imageRect.y, + imageRect.width, imageRect.height, size.width, + size.height, + uri ? uri->GetSpecOrDefault().get() : "N/A")); + + AutoRestoreSVGState autoRestore(svgContext, animTime, mSVGDocumentWrapper, + contextPaint); + + mSVGDocumentWrapper->UpdateViewportBounds(viewportSize); + mSVGDocumentWrapper->FlushImageTransformInvalidation(); + + gfxContext ctx(dt); + + nsRect svgRect; + auto auPerDevPixel = presContext->AppUnitsPerDevPixel(); + if (size != viewportSize) { + auto scaleX = double(size.width) / viewportSize.width; + auto scaleY = double(size.height) / viewportSize.height; + ctx.SetMatrix(Matrix::Scaling(float(scaleX), float(scaleY))); + + auto scaledVisibleRect = IntRectToRect(imageRect); + scaledVisibleRect.Scale(float(auPerDevPixel / scaleX), + float(auPerDevPixel / scaleY)); + scaledVisibleRect.Round(); + svgRect.SetRect( + int32_t(scaledVisibleRect.x), int32_t(scaledVisibleRect.y), + int32_t(scaledVisibleRect.width), int32_t(scaledVisibleRect.height)); + } else { + auto scaledVisibleRect(imageRect); + scaledVisibleRect.Scale(auPerDevPixel); + svgRect.SetRect(scaledVisibleRect.x, scaledVisibleRect.y, + scaledVisibleRect.width, scaledVisibleRect.height); + } + + RenderDocumentFlags renderDocFlags = + RenderDocumentFlags::IgnoreViewportScrolling; + if (!(mImageFlags & imgIContainer::FLAG_SYNC_DECODE)) { + renderDocFlags |= RenderDocumentFlags::AsyncDecodeImages; + } + if (mImageFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING) { + renderDocFlags |= RenderDocumentFlags::UseHighQualityScaling; + } + + presShell->RenderDocument(svgRect, renderDocFlags, + NS_RGBA(0, 0, 0, 0), // transparent + &ctx); + } + + recorder->FlushItem(imageRectOrigin); + recorder->Finish(); + + if (!validFonts) { + gfxCriticalNote << "Failed serializing fonts for blob vector image"; + return Nothing(); + } + + Range bytes((uint8_t*)recorder->mOutputStream.mData, + recorder->mOutputStream.mLength); + wr::BlobImageKey key = aBlobKey + ? aBlobKey.value() + : wr::BlobImageKey{wrBridge->GetNextImageKey()}; + wr::ImageDescriptor descriptor(imageRect.Size(), 0, SurfaceFormat::OS_RGBA, + wr::OpacityType::HasAlphaChannel); + + auto visibleRect = ImageIntRect::FromUnknownRect(imageRectOrigin); + if (aBlobKey) { + if (!aResources.UpdateBlobImage(key, descriptor, bytes, visibleRect, + visibleRect)) { + return Nothing(); + } + } else if (!aResources.AddBlobImage(key, descriptor, bytes, visibleRect)) { + return Nothing(); + } + + std::vector> externalSurfaces; + recorder->TakeExternalSurfaces(externalSurfaces); + + for (auto& surface : externalSurfaces) { + // While we don't use the image key with the surface, because the blob image + // renderer doesn't have easy access to the resource set, we still want to + // ensure one is generated. That will ensure the surface remains alive until + // at least the last epoch which the blob image could be used in. + wr::ImageKey key = {}; + DebugOnly rv = + SharedSurfacesChild::Share(surface, rootManager, aResources, key); + MOZ_ASSERT(rv.value != NS_ERROR_NOT_IMPLEMENTED); + } + + return Some(BlobImageKeyData(aManager, key, std::move(fonts), + std::move(externalSurfaces))); +} + +} // namespace mozilla::image diff --git a/image/BlobSurfaceProvider.h b/image/BlobSurfaceProvider.h new file mode 100644 index 0000000000..6515bb2af4 --- /dev/null +++ b/image/BlobSurfaceProvider.h @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_IMAGE_BLOBSURFACEPROVIDER_H_ +#define MOZILLA_IMAGE_BLOBSURFACEPROVIDER_H_ + +#include "mozilla/Maybe.h" +#include "mozilla/SVGImageContext.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "ImageRegion.h" +#include "ISurfaceProvider.h" + +#include + +namespace mozilla { +namespace image { + +class BlobImageKeyData final { + public: + BlobImageKeyData(layers::WebRenderLayerManager* aManager, + const wr::BlobImageKey& aBlobKey, + std::vector>&& aScaledFonts, + std::vector>&& aExternalSurfaces) + : mManager(aManager), + mBlobKey(aBlobKey), + mScaledFonts(std::move(aScaledFonts)), + mExternalSurfaces(std::move(aExternalSurfaces)), + mDirty(false) {} + + BlobImageKeyData(BlobImageKeyData&& aOther) noexcept + : mManager(std::move(aOther.mManager)), + mBlobKey(aOther.mBlobKey), + mScaledFonts(std::move(aOther.mScaledFonts)), + mExternalSurfaces(std::move(aOther.mExternalSurfaces)), + mDirty(aOther.mDirty) {} + + BlobImageKeyData& operator=(BlobImageKeyData&& aOther) noexcept { + mManager = std::move(aOther.mManager); + mBlobKey = aOther.mBlobKey; + mScaledFonts = std::move(aOther.mScaledFonts); + mExternalSurfaces = std::move(aOther.mExternalSurfaces); + mDirty = aOther.mDirty; + return *this; + } + + BlobImageKeyData(const BlobImageKeyData&) = delete; + BlobImageKeyData& operator=(const BlobImageKeyData&) = delete; + + RefPtr mManager; + wr::BlobImageKey mBlobKey; + std::vector> mScaledFonts; + std::vector> mExternalSurfaces; + bool mDirty; +}; + +} // namespace image +} // namespace mozilla + +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::image::BlobImageKeyData); + +namespace mozilla { +namespace wr { +class IpcResourceUpdateQueue; +} // namespace wr + +namespace image { +class SVGDocumentWrapper; + +/** + * An ISurfaceProvider that manages blob recordings of SVG images. Unlike the + * rasterized ISurfaceProviders, it only provides a recording which may be + * replayed in the compositor process by WebRender. It may be invalidated + * directly in order to reuse the resource ids and underlying buffers when the + * SVG image has changed (e.g. it is animated). + */ +class BlobSurfaceProvider final : public ISurfaceProvider { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BlobSurfaceProvider, override) + + BlobSurfaceProvider(ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + SVGDocumentWrapper* aSVGDocumentWrapper, + uint32_t aImageFlags); + + bool IsFinished() const override { return true; } + + size_t LogicalSizeInBytes() const override { + const gfx::IntSize& size = GetSurfaceKey().Size(); + return size.width * size.height * sizeof(uint32_t); + } + + nsresult UpdateKey(layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + wr::ImageKey& aKey) override; + + void InvalidateRecording() override; + + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) override { + AddSizeOfCbData metadata; + metadata.mFinished = true; + metadata.mHeapBytes += mKeys.ShallowSizeOfExcludingThis(aMallocSizeOf); + + gfx::SourceSurface::SizeOfInfo info; + info.AddType(gfx::SurfaceType::BLOB_IMAGE); + metadata.Accumulate(info); + + aCallback(metadata); + } + + protected: + DrawableFrameRef DrawableRef(size_t aFrame) override { + MOZ_ASSERT_UNREACHABLE("BlobSurfaceProvider::DrawableRef not supported!"); + return DrawableFrameRef(); + } + bool IsLocked() const override { return true; } + void SetLocked(bool) override {} + + private: + ~BlobSurfaceProvider() override; + + Maybe RecordDrawing(layers::WebRenderLayerManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + Maybe aBlobKey); + + static void DestroyKeys(const AutoTArray& aKeys); + + AutoTArray mKeys; + + RefPtr mSVGDocumentWrapper; + uint32_t mImageFlags; +}; + +} // namespace image +} // namespace mozilla + +#endif /* MOZILLA_IMAGE_BLOBSURFACEPROVIDER_H_ */ diff --git a/image/ClippedImage.cpp b/image/ClippedImage.cpp new file mode 100644 index 0000000000..cad74ebdf8 --- /dev/null +++ b/image/ClippedImage.cpp @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ClippedImage.h" + +#include +#include +#include // Workaround for bug in VS10; see bug 981264. +#include + +#include "ImageRegion.h" +#include "Orientation.h" +#include "gfxContext.h" +#include "gfxDrawable.h" +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGImageContext.h" + +#include "mozilla/gfx/2D.h" + +namespace mozilla { + +using namespace gfx; +using std::max; + +namespace image { + +class ClippedImageCachedSurface { + public: + ClippedImageCachedSurface(already_AddRefed aSurface, + const nsIntSize& aSize, + const SVGImageContext& aSVGContext, float aFrame, + uint32_t aFlags, ImgDrawResult aDrawResult) + : mSurface(aSurface), + mSize(aSize), + mSVGContext(aSVGContext), + mFrame(aFrame), + mFlags(aFlags), + mDrawResult(aDrawResult) { + MOZ_ASSERT(mSurface, "Must have a valid surface"); + } + + bool Matches(const nsIntSize& aSize, const SVGImageContext& aSVGContext, + float aFrame, uint32_t aFlags) const { + return mSize == aSize && mSVGContext == aSVGContext && mFrame == aFrame && + mFlags == aFlags; + } + + already_AddRefed Surface() const { + RefPtr surf(mSurface); + return surf.forget(); + } + + ImgDrawResult GetDrawResult() const { return mDrawResult; } + + bool NeedsRedraw() const { + return mDrawResult != ImgDrawResult::SUCCESS && + mDrawResult != ImgDrawResult::BAD_IMAGE; + } + + private: + RefPtr mSurface; + const nsIntSize mSize; + SVGImageContext mSVGContext; + const float mFrame; + const uint32_t mFlags; + const ImgDrawResult mDrawResult; +}; + +class DrawSingleTileCallback : public gfxDrawingCallback { + public: + DrawSingleTileCallback(ClippedImage* aImage, const nsIntSize& aSize, + const SVGImageContext& aSVGContext, + uint32_t aWhichFrame, uint32_t aFlags, float aOpacity) + : mImage(aImage), + mSize(aSize), + mSVGContext(aSVGContext), + mWhichFrame(aWhichFrame), + mFlags(aFlags), + mDrawResult(ImgDrawResult::NOT_READY), + mOpacity(aOpacity) { + MOZ_ASSERT(mImage, "Must have an image to clip"); + } + + virtual bool operator()(gfxContext* aContext, const gfxRect& aFillRect, + const SamplingFilter aSamplingFilter, + const gfxMatrix& aTransform) override { + MOZ_ASSERT(aTransform.IsIdentity(), + "Caller is probably CreateSamplingRestrictedDrawable, " + "which should not happen"); + + // Draw the image. |gfxCallbackDrawable| always calls this function with + // arguments that guarantee we never tile. + mDrawResult = mImage->DrawSingleTile( + aContext, mSize, ImageRegion::Create(aFillRect), mWhichFrame, + aSamplingFilter, mSVGContext, mFlags, mOpacity); + + return true; + } + + ImgDrawResult GetDrawResult() { return mDrawResult; } + + private: + RefPtr mImage; + const nsIntSize mSize; + const SVGImageContext& mSVGContext; + const uint32_t mWhichFrame; + const uint32_t mFlags; + ImgDrawResult mDrawResult; + float mOpacity; +}; + +ClippedImage::ClippedImage(Image* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize) + : ImageWrapper(aImage), mClip(aClip) { + MOZ_ASSERT(aImage != nullptr, "ClippedImage requires an existing Image"); + MOZ_ASSERT_IF(aSVGViewportSize, + aImage->GetType() == imgIContainer::TYPE_VECTOR); + if (aSVGViewportSize) { + mSVGViewportSize = + Some(aSVGViewportSize->ToNearestPixels(AppUnitsPerCSSPixel())); + } +} + +ClippedImage::~ClippedImage() {} + +bool ClippedImage::ShouldClip() { + // We need to evaluate the clipping region against the image's width and + // height once they're available to determine if it's valid and whether we + // actually need to do any work. We may fail if the image's width and height + // aren't available yet, in which case we'll try again later. + if (mShouldClip.isNothing()) { + int32_t width, height; + RefPtr progressTracker = + InnerImage()->GetProgressTracker(); + if (InnerImage()->HasError()) { + // If there's a problem with the inner image we'll let it handle + // everything. + mShouldClip.emplace(false); + } else if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) { + // Clamp the clipping region to the size of the SVG viewport. + nsIntRect svgViewportRect(nsIntPoint(0, 0), *mSVGViewportSize); + + mClip = mClip.Intersect(svgViewportRect); + + // If the clipping region is the same size as the SVG viewport size + // we don't have to do anything. + mShouldClip.emplace(!mClip.IsEqualInterior(svgViewportRect)); + } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&width)) && width > 0 && + NS_SUCCEEDED(InnerImage()->GetHeight(&height)) && height > 0) { + // Clamp the clipping region to the size of the underlying image. + mClip = mClip.Intersect(nsIntRect(0, 0, width, height)); + + // If the clipping region is the same size as the underlying image we + // don't have to do anything. + mShouldClip.emplace( + !mClip.IsEqualInterior(nsIntRect(0, 0, width, height))); + } else if (progressTracker && + !(progressTracker->GetProgress() & FLAG_LOAD_COMPLETE)) { + // The image just hasn't finished loading yet. We don't yet know whether + // clipping with be needed or not for now. Just return without memorizing + // anything. + return false; + } else { + // We have a fully loaded image without a clearly defined width and + // height. This can happen with SVG images. + mShouldClip.emplace(false); + } + } + + MOZ_ASSERT(mShouldClip.isSome(), "Should have computed a result"); + return *mShouldClip; +} + +NS_IMETHODIMP +ClippedImage::GetWidth(int32_t* aWidth) { + if (!ShouldClip()) { + return InnerImage()->GetWidth(aWidth); + } + + *aWidth = mClip.Width(); + return NS_OK; +} + +NS_IMETHODIMP +ClippedImage::GetHeight(int32_t* aHeight) { + if (!ShouldClip()) { + return InnerImage()->GetHeight(aHeight); + } + + *aHeight = mClip.Height(); + return NS_OK; +} + +NS_IMETHODIMP +ClippedImage::GetIntrinsicSize(nsSize* aSize) { + if (!ShouldClip()) { + return InnerImage()->GetIntrinsicSize(aSize); + } + + *aSize = nsSize(mClip.Width(), mClip.Height()); + return NS_OK; +} + +Maybe ClippedImage::GetIntrinsicRatio() { + if (!ShouldClip()) { + return InnerImage()->GetIntrinsicRatio(); + } + + return Some(AspectRatio::FromSize(mClip.Width(), mClip.Height())); +} + +NS_IMETHODIMP_(already_AddRefed) +ClippedImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + RefPtr surface; + std::tie(std::ignore, surface) = GetFrameInternal( + mClip.Size(), SVGImageContext(), Nothing(), aWhichFrame, aFlags, 1.0); + return surface.forget(); +} + +NS_IMETHODIMP_(already_AddRefed) +ClippedImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + // XXX(seth): It'd be nice to support downscale-during-decode for this case, + // but right now we just fall back to the intrinsic size. + return GetFrame(aWhichFrame, aFlags); +} + +std::pair> ClippedImage::GetFrameInternal( + const nsIntSize& aSize, const SVGImageContext& aSVGContext, + const Maybe& aRegion, uint32_t aWhichFrame, uint32_t aFlags, + float aOpacity) { + if (!ShouldClip()) { + RefPtr surface = InnerImage()->GetFrame(aWhichFrame, aFlags); + return std::make_pair( + surface ? ImgDrawResult::SUCCESS : ImgDrawResult::NOT_READY, + std::move(surface)); + } + + float frameToDraw = InnerImage()->GetFrameIndex(aWhichFrame); + if (!mCachedSurface || + !mCachedSurface->Matches(aSize, aSVGContext, frameToDraw, aFlags) || + mCachedSurface->NeedsRedraw()) { + // Create a surface to draw into. + RefPtr target = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + IntSize(aSize.width, aSize.height), SurfaceFormat::OS_RGBA); + if (!target || !target->IsValid()) { + NS_ERROR("Could not create a DrawTarget"); + return std::make_pair(ImgDrawResult::TEMPORARY_ERROR, + RefPtr()); + } + + gfxContext ctx(target); + + // Create our callback. + RefPtr drawTileCallback = + new DrawSingleTileCallback(this, aSize, aSVGContext, aWhichFrame, + aFlags, aOpacity); + RefPtr drawable = + new gfxCallbackDrawable(drawTileCallback, aSize); + + // Actually draw. The callback will end up invoking DrawSingleTile. + gfxUtils::DrawPixelSnapped(&ctx, drawable, SizeDouble(aSize), + ImageRegion::Create(aSize), + SurfaceFormat::OS_RGBA, SamplingFilter::LINEAR, + imgIContainer::FLAG_CLAMP); + + // Cache the resulting surface. + mCachedSurface = MakeUnique( + target->Snapshot(), aSize, aSVGContext, frameToDraw, aFlags, + drawTileCallback->GetDrawResult()); + } + + MOZ_ASSERT(mCachedSurface, "Should have a cached surface now"); + RefPtr surface = mCachedSurface->Surface(); + return std::make_pair(mCachedSurface->GetDrawResult(), std::move(surface)); +} + +NS_IMETHODIMP_(bool) +ClippedImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + if (!ShouldClip()) { + return InnerImage()->IsImageContainerAvailable(aRenderer, aFlags); + } + return false; +} + +NS_IMETHODIMP_(ImgDrawResult) +ClippedImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + // XXX(seth): We currently don't have a way of clipping the result of + // GetImageContainer. We work around this by always returning null, but if it + // ever turns out that ClippedImage is widely used on codepaths that can + // actually benefit from GetImageContainer, it would be a good idea to fix + // that method for performance reasons. + + if (!ShouldClip()) { + return InnerImage()->GetImageProvider(aRenderer, aSize, aSVGContext, + aRegion, aFlags, aProvider); + } + + return ImgDrawResult::NOT_SUPPORTED; +} + +static bool MustCreateSurface(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, + const uint32_t aFlags) { + gfxRect imageRect(0, 0, aSize.width, aSize.height); + bool willTile = !imageRect.Contains(aRegion.Rect()) && + !(aFlags & imgIContainer::FLAG_CLAMP); + bool willResample = aContext->CurrentMatrix().HasNonIntegerTranslation() && + (willTile || !aRegion.RestrictionContains(imageRect)); + return willTile || willResample; +} + +NS_IMETHODIMP_(ImgDrawResult) +ClippedImage::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + if (!ShouldClip()) { + return InnerImage()->Draw(aContext, aSize, aRegion, aWhichFrame, + aSamplingFilter, aSVGContext, aFlags, aOpacity); + } + + // Check for tiling. If we need to tile then we need to create a + // gfxCallbackDrawable to handle drawing for us. + if (MustCreateSurface(aContext, aSize, aRegion, aFlags)) { + // Create a temporary surface containing a single tile of this image. + // GetFrame will call DrawSingleTile internally. + auto [result, surface] = GetFrameInternal(aSize, aSVGContext, Nothing(), + aWhichFrame, aFlags, aOpacity); + if (!surface) { + MOZ_ASSERT(result != ImgDrawResult::SUCCESS); + return result; + } + + // Create a drawable from that surface. + RefPtr drawable = + new gfxSurfaceDrawable(surface, aSize); + + // Draw. + gfxUtils::DrawPixelSnapped(aContext, drawable, SizeDouble(aSize), aRegion, + SurfaceFormat::OS_RGBA, aSamplingFilter, + aOpacity); + + return result; + } + + return DrawSingleTile(aContext, aSize, aRegion, aWhichFrame, aSamplingFilter, + aSVGContext, aFlags, aOpacity); +} + +ImgDrawResult ClippedImage::DrawSingleTile( + gfxContext* aContext, const nsIntSize& aSize, const ImageRegion& aRegion, + uint32_t aWhichFrame, SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, float aOpacity) { + MOZ_ASSERT(!MustCreateSurface(aContext, aSize, aRegion, aFlags), + "Shouldn't need to create a surface"); + + gfxRect clip(mClip.X(), mClip.Y(), mClip.Width(), mClip.Height()); + nsIntSize size(aSize), innerSize(aSize); + bool needScale = false; + if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) { + innerSize = *mSVGViewportSize; + needScale = true; + } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&innerSize.width)) && + NS_SUCCEEDED(InnerImage()->GetHeight(&innerSize.height))) { + needScale = true; + } else { + MOZ_ASSERT_UNREACHABLE( + "If ShouldClip() led us to draw then we should never get here"); + } + + if (needScale) { + double scaleX = aSize.width / clip.Width(); + double scaleY = aSize.height / clip.Height(); + + // Map the clip and size to the scale requested by the caller. + clip.Scale(scaleX, scaleY); + size = innerSize; + size.Scale(scaleX, scaleY); + } + + // We restrict our drawing to only the clipping region, and translate so that + // the clipping region is placed at the position the caller expects. + ImageRegion region(aRegion); + region.MoveBy(clip.X(), clip.Y()); + region = region.Intersect(clip); + + gfxContextMatrixAutoSaveRestore saveMatrix(aContext); + aContext->Multiply(gfxMatrix::Translation(-clip.X(), -clip.Y())); + + auto unclipViewport = [&](const SVGImageContext& aOldContext) { + // Map the viewport to the inner image. Note that we don't take the aSize + // parameter of imgIContainer::Draw into account, just the clipping region. + // The size in pixels at which the output will ultimately be drawn is + // irrelevant here since the purpose of the SVG viewport size is to + // determine what *region* of the SVG document will be drawn. + SVGImageContext context(aOldContext); + auto oldViewport = aOldContext.GetViewportSize(); + if (oldViewport) { + CSSIntSize newViewport; + newViewport.width = + ceil(oldViewport->width * double(innerSize.width) / mClip.Width()); + newViewport.height = + ceil(oldViewport->height * double(innerSize.height) / mClip.Height()); + context.SetViewportSize(Some(newViewport)); + } + return context; + }; + + return InnerImage()->Draw(aContext, size, region, aWhichFrame, + aSamplingFilter, unclipViewport(aSVGContext), + aFlags, aOpacity); +} + +NS_IMETHODIMP +ClippedImage::RequestDiscard() { + // We're very aggressive about discarding. + mCachedSurface = nullptr; + + return InnerImage()->RequestDiscard(); +} + +NS_IMETHODIMP_(Orientation) +ClippedImage::GetOrientation() { + // XXX(seth): This should not actually be here; this is just to work around a + // what appears to be a bug in MSVC's linker. + return InnerImage()->GetOrientation(); +} + +nsIntSize ClippedImage::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + if (!ShouldClip()) { + return InnerImage()->OptimalImageSizeForDest(aDest, aWhichFrame, + aSamplingFilter, aFlags); + } + + int32_t imgWidth, imgHeight; + bool needScale = false; + bool forceUniformScaling = false; + if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) { + imgWidth = mSVGViewportSize->width; + imgHeight = mSVGViewportSize->height; + needScale = true; + forceUniformScaling = (aFlags & imgIContainer::FLAG_FORCE_UNIFORM_SCALING); + } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&imgWidth)) && + NS_SUCCEEDED(InnerImage()->GetHeight(&imgHeight))) { + needScale = true; + } + + if (needScale) { + // To avoid ugly sampling artifacts, ClippedImage needs the image size to + // be chosen such that the clipping region lies on pixel boundaries. + + // First, we select a scale that's good for ClippedImage. An integer + // multiple of the size of the clipping region is always fine. + IntSize scale = IntSize::Ceil(aDest.width / mClip.Width(), + aDest.height / mClip.Height()); + + if (forceUniformScaling) { + scale.width = scale.height = max(scale.height, scale.width); + } + + // Determine the size we'd prefer to render the inner image at, and ask the + // inner image what size we should actually use. + gfxSize desiredSize(double(imgWidth) * scale.width, + double(imgHeight) * scale.height); + nsIntSize innerDesiredSize = InnerImage()->OptimalImageSizeForDest( + desiredSize, aWhichFrame, aSamplingFilter, aFlags); + + // To get our final result, we take the inner image's desired size and + // determine how large the clipped region would be at that scale. (Again, we + // ensure an integer multiple of the size of the clipping region.) + IntSize finalScale = + IntSize::Ceil(double(innerDesiredSize.width) / imgWidth, + double(innerDesiredSize.height) / imgHeight); + return mClip.Size() * finalScale; + } + + MOZ_ASSERT(false, + "If ShouldClip() led us to draw then we should never get here"); + return InnerImage()->OptimalImageSizeForDest(aDest, aWhichFrame, + aSamplingFilter, aFlags); +} + +NS_IMETHODIMP_(nsIntRect) +ClippedImage::GetImageSpaceInvalidationRect(const nsIntRect& aRect) { + if (!ShouldClip()) { + return InnerImage()->GetImageSpaceInvalidationRect(aRect); + } + + nsIntRect rect(InnerImage()->GetImageSpaceInvalidationRect(aRect)); + rect = rect.Intersect(mClip); + rect.MoveBy(-mClip.X(), -mClip.Y()); + return rect; +} + +} // namespace image +} // namespace mozilla diff --git a/image/ClippedImage.h b/image/ClippedImage.h new file mode 100644 index 0000000000..43295a53f6 --- /dev/null +++ b/image/ClippedImage.h @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ClippedImage_h +#define mozilla_image_ClippedImage_h + +#include "ImageWrapper.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +#include + +namespace mozilla { +namespace image { + +class ClippedImageCachedSurface; +class DrawSingleTileCallback; + +/** + * An Image wrapper that clips an image against a rectangle. Right now only + * absolute coordinates in pixels are supported. + * + * XXX(seth): There a known (performance, not correctness) issue with + * GetImageContainer. See the comments for that method for more information. + */ +class ClippedImage : public ImageWrapper { + typedef gfx::SourceSurface SourceSurface; + + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(ClippedImage, ImageWrapper) + + NS_IMETHOD GetWidth(int32_t* aWidth) override; + NS_IMETHOD GetHeight(int32_t* aHeight) override; + NS_IMETHOD GetIntrinsicSize(nsSize* aSize) override; + Maybe GetIntrinsicRatio() override; + NS_IMETHOD_(already_AddRefed) + GetFrame(uint32_t aWhichFrame, uint32_t aFlags) override; + NS_IMETHOD_(already_AddRefed) + GetFrameAtSize(const gfx::IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) override; + NS_IMETHOD_(bool) + IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) override; + NS_IMETHOD_(ImgDrawResult) + GetImageProvider(WindowRenderer* aRenderer, const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, uint32_t aFlags, + WebRenderImageProvider** aProvider) override; + NS_IMETHOD_(ImgDrawResult) + Draw(gfxContext* aContext, const nsIntSize& aSize, const ImageRegion& aRegion, + uint32_t aWhichFrame, gfx::SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) override; + NS_IMETHOD RequestDiscard() override; + NS_IMETHOD_(Orientation) GetOrientation() override; + NS_IMETHOD_(nsIntRect) + GetImageSpaceInvalidationRect(const nsIntRect& aRect) override; + nsIntSize OptimalImageSizeForDest(const gfxSize& aDest, uint32_t aWhichFrame, + gfx::SamplingFilter aSamplingFilter, + uint32_t aFlags) override; + + protected: + ClippedImage(Image* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize); + + virtual ~ClippedImage(); + + private: + std::pair> GetFrameInternal( + const nsIntSize& aSize, const SVGImageContext& aSVGContext, + const Maybe& aRegion, uint32_t aWhichFrame, + uint32_t aFlags, float aOpacity); + bool ShouldClip(); + ImgDrawResult DrawSingleTile(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + gfx::SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, + uint32_t aFlags, float aOpacity); + + // If we are forced to draw a temporary surface, we cache it here. + UniquePtr mCachedSurface; + + nsIntRect mClip; // The region to clip to. + Maybe mShouldClip; // Memoized ShouldClip() if present. + Maybe mSVGViewportSize; // If we're clipping a VectorImage, this + // is the size of viewport of that image. + friend class DrawSingleTileCallback; + friend class ImageOps; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ClippedImage_h diff --git a/image/CopyOnWrite.h b/image/CopyOnWrite.h new file mode 100644 index 0000000000..af134d581a --- /dev/null +++ b/image/CopyOnWrite.h @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * CopyOnWrite allows code to safely read from a data structure without + * worrying that reentrant code will modify it. + */ + +#ifndef mozilla_image_CopyOnWrite_h +#define mozilla_image_CopyOnWrite_h + +#include "mozilla/RefPtr.h" +#include "MainThreadUtils.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// Implementation Details +/////////////////////////////////////////////////////////////////////////////// + +namespace detail { + +template +class CopyOnWriteValue final { + public: + NS_INLINE_DECL_REFCOUNTING(CopyOnWriteValue) + + explicit CopyOnWriteValue(T* aValue) + : mValue(aValue), mReaders(0), mWriter(false) {} + explicit CopyOnWriteValue(already_AddRefed& aValue) + : mValue(aValue), mReaders(0), mWriter(false) {} + explicit CopyOnWriteValue(already_AddRefed&& aValue) + : mValue(aValue), mReaders(0), mWriter(false) {} + explicit CopyOnWriteValue(const RefPtr& aValue) + : mValue(aValue), mReaders(0), mWriter(false) {} + explicit CopyOnWriteValue(RefPtr&& aValue) + : mValue(aValue), mReaders(0), mWriter(false) {} + + T* get() { return mValue.get(); } + const T* get() const { return mValue.get(); } + + bool HasReaders() const { return mReaders > 0; } + bool HasWriter() const { return mWriter; } + bool HasUsers() const { return HasReaders() || HasWriter(); } + + void LockForReading() { + MOZ_ASSERT(!HasWriter()); + mReaders++; + } + void UnlockForReading() { + MOZ_ASSERT(HasReaders()); + mReaders--; + } + + struct MOZ_STACK_CLASS AutoReadLock { + explicit AutoReadLock(CopyOnWriteValue* aValue) : mValue(aValue) { + mValue->LockForReading(); + } + ~AutoReadLock() { mValue->UnlockForReading(); } + CopyOnWriteValue* mValue; + }; + + void LockForWriting() { + MOZ_ASSERT(!HasUsers()); + mWriter = true; + } + void UnlockForWriting() { + MOZ_ASSERT(HasWriter()); + mWriter = false; + } + + struct MOZ_STACK_CLASS AutoWriteLock { + explicit AutoWriteLock(CopyOnWriteValue* aValue) : mValue(aValue) { + mValue->LockForWriting(); + } + ~AutoWriteLock() { mValue->UnlockForWriting(); } + CopyOnWriteValue* mValue; + }; + + private: + CopyOnWriteValue(const CopyOnWriteValue&) = delete; + CopyOnWriteValue(CopyOnWriteValue&&) = delete; + + ~CopyOnWriteValue() {} + + RefPtr mValue; + uint64_t mReaders = 0; + bool mWriter = false; +}; + +} // namespace detail + +/////////////////////////////////////////////////////////////////////////////// +// Public API +/////////////////////////////////////////////////////////////////////////////// + +/** + * CopyOnWrite allows code to safely read from a data structure without + * worrying that reentrant code will modify it. If reentrant code would modify + * the data structure while other code is reading from it, a copy is made so + * that readers can continue to use the old version. + * + * Note that it's legal to nest a writer inside any number of readers, but + * nothing can be nested inside a writer. This is because it's assumed that the + * state of the contained data structure may not be consistent during the write. + * + * This is a main-thread-only data structure. + * + * To work with CopyOnWrite, a type T needs to be reference counted and to + * support copy construction. + */ +template +class CopyOnWrite final { + typedef detail::CopyOnWriteValue CopyOnWriteValue; + + public: + explicit CopyOnWrite(T* aValue) : mValue(new CopyOnWriteValue(aValue)) {} + + explicit CopyOnWrite(already_AddRefed& aValue) + : mValue(new CopyOnWriteValue(aValue)) {} + + explicit CopyOnWrite(already_AddRefed&& aValue) + : mValue(new CopyOnWriteValue(aValue)) {} + + explicit CopyOnWrite(const RefPtr& aValue) + : mValue(new CopyOnWriteValue(aValue)) {} + + explicit CopyOnWrite(RefPtr&& aValue) + : mValue(new CopyOnWriteValue(aValue)) {} + + /// @return true if it's safe to read at this time. + bool CanRead() const { return !mValue->HasWriter(); } + + /** + * Read from the contained data structure using the function @aReader. + * @aReader will be passed a pointer of type |const T*|. It's not legal to + * call this while a writer is active. + * + * @return whatever value @aReader returns, or nothing if @aReader is a void + * function. + */ + template + auto Read(ReadFunc aReader) const + -> decltype(aReader(static_cast(nullptr))) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(CanRead()); + + // Run the provided function while holding a read lock. + RefPtr cowValue = mValue; + typename CopyOnWriteValue::AutoReadLock lock(cowValue); + return aReader(cowValue->get()); + } + + /** + * Read from the contained data structure using the function @aReader. + * @aReader will be passed a pointer of type |const T*|. If it's currently not + * possible to read because a writer is currently active, @aOnError will be + * called instead. + * + * @return whatever value @aReader or @aOnError returns (their return types + * must be consistent), or nothing if the provided functions are void. + */ + template + auto Read(ReadFunc aReader, ErrorFunc aOnError) const + -> decltype(aReader(static_cast(nullptr))) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!CanRead()) { + return aOnError(); + } + + return Read(aReader); + } + + /// @return true if it's safe to write at this time. + bool CanWrite() const { return !mValue->HasWriter(); } + + /** + * Write to the contained data structure using the function @aWriter. + * @aWriter will be passed a pointer of type |T*|. It's not legal to call this + * while another writer is active. + * + * If readers are currently active, they will be able to continue reading from + * a copy of the old version of the data structure. The copy will be destroyed + * when all its readers finish. Later readers and writers will see the + * version of the data structure produced by the most recent call to Write(). + * + * @return whatever value @aWriter returns, or nothing if @aWriter is a void + * function. + */ + template + auto Write(WriteFunc aWriter) -> decltype(aWriter(static_cast(nullptr))) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(CanWrite()); + + // If there are readers, we need to copy first. + if (mValue->HasReaders()) { + mValue = new CopyOnWriteValue(new T(*mValue->get())); + } + + // Run the provided function while holding a write lock. + RefPtr cowValue = mValue; + typename CopyOnWriteValue::AutoWriteLock lock(cowValue); + return aWriter(cowValue->get()); + } + + /** + * Write to the contained data structure using the function @aWriter. + * @aWriter will be passed a pointer of type |T*|. If it's currently not + * possible to write because a writer is currently active, @aOnError will be + * called instead. + * + * If readers are currently active, they will be able to continue reading from + * a copy of the old version of the data structure. The copy will be destroyed + * when all its readers finish. Later readers and writers will see the + * version of the data structure produced by the most recent call to Write(). + * + * @return whatever value @aWriter or @aOnError returns (their return types + * must be consistent), or nothing if the provided functions are void. + */ + template + auto Write(WriteFunc aWriter, ErrorFunc aOnError) + -> decltype(aWriter(static_cast(nullptr))) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!CanWrite()) { + return aOnError(); + } + + return Write(aWriter); + } + + private: + CopyOnWrite(const CopyOnWrite&) = delete; + CopyOnWrite(CopyOnWrite&&) = delete; + + RefPtr mValue; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_CopyOnWrite_h diff --git a/image/DecodePool.cpp b/image/DecodePool.cpp new file mode 100644 index 0000000000..753e53727c --- /dev/null +++ b/image/DecodePool.cpp @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DecodePool.h" + +#include + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Monitor.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/TaskController.h" +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsThreadManager.h" +#include "nsThreadUtils.h" +#include "nsXPCOMCIDInternal.h" +#include "prsystem.h" + +#include "Decoder.h" +#include "IDecodingTask.h" +#include "RasterImage.h" + +#if defined(XP_WIN) +# include +# include "mozilla/WindowsProcessMitigations.h" +#endif + +using std::max; +using std::min; + +namespace mozilla { +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// DecodePool implementation. +/////////////////////////////////////////////////////////////////////////////// + +/* static */ +StaticRefPtr DecodePool::sSingleton; +/* static */ +uint32_t DecodePool::sNumCores = 0; + +NS_IMPL_ISUPPORTS(DecodePool, nsIObserver) + +/* static */ +void DecodePool::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + sNumCores = max(PR_GetNumberOfProcessors(), 1); + DecodePool::Singleton(); +} + +/* static */ +DecodePool* DecodePool::Singleton() { + if (!sSingleton) { + MOZ_ASSERT(NS_IsMainThread()); + sSingleton = new DecodePool(); + ClearOnShutdown(&sSingleton); + } + + return sSingleton; +} + +/* static */ +uint32_t DecodePool::NumberOfCores() { return sNumCores; } + +#if defined(XP_WIN) +class IOThreadIniter final : public Runnable { + public: + explicit IOThreadIniter() : Runnable("image::IOThreadIniter") {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + + CoInitialize(nullptr); + + return NS_OK; + } +}; +#endif + +DecodePool::DecodePool() : mMutex("image::IOThread") { + // Initialize the I/O thread. +#if defined(XP_WIN) + // On Windows we use the io thread to get icons from the system. Any thread + // that makes system calls needs to call CoInitialize. And these system calls + // (SHGetFileInfo) should only be called from one thread at a time, in case + // we ever create more than one io thread. If win32k is locked down, we can't + // call SHGetFileInfo anyway, so we don't need the initializer. + nsCOMPtr initer = + IsWin32kLockedDown() ? nullptr : new IOThreadIniter(); + nsresult rv = NS_NewNamedThread("ImageIO", getter_AddRefs(mIOThread), initer); +#else + nsresult rv = NS_NewNamedThread("ImageIO", getter_AddRefs(mIOThread)); +#endif + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv) && mIOThread, + "Should successfully create image I/O thread"); + + nsCOMPtr obsSvc = services::GetObserverService(); + if (obsSvc) { + obsSvc->AddObserver(this, "xpcom-shutdown-threads", false); + } +} + +DecodePool::~DecodePool() { + MOZ_ASSERT(NS_IsMainThread(), "Must shut down DecodePool on main thread!"); +} + +NS_IMETHODIMP +DecodePool::Observe(nsISupports*, const char* aTopic, const char16_t*) { + MOZ_ASSERT(strcmp(aTopic, "xpcom-shutdown-threads") == 0, "Unexpected topic"); + + mShuttingDown = true; + + nsCOMPtr ioThread; + + { + MutexAutoLock lock(mMutex); + ioThread.swap(mIOThread); + } + + if (ioThread) { + ioThread->Shutdown(); + } + + return NS_OK; +} + +bool DecodePool::IsShuttingDown() const { return mShuttingDown; } + +class DecodingTask final : public Task { + public: + explicit DecodingTask(RefPtr&& aTask) + : Task(false, aTask->Priority() == TaskPriority::eLow + ? EventQueuePriority::Normal + : EventQueuePriority::RenderBlocking), + mTask(aTask) {} + + bool Run() override { + mTask->Run(); + return true; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + bool GetName(nsACString& aName) override { + aName.AssignLiteral("ImageDecodingTask"); + return true; + } +#endif + + private: + RefPtr mTask; +}; + +void DecodePool::AsyncRun(IDecodingTask* aTask) { + MOZ_ASSERT(aTask); + + TaskController::Get()->AddTask( + MakeAndAddRef((RefPtr(aTask)))); +} + +bool DecodePool::SyncRunIfPreferred(IDecodingTask* aTask, + const nsCString& aURI) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTask); + + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("DecodePool::SyncRunIfPreferred", + GRAPHICS, aURI); + + if (aTask->ShouldPreferSyncRun()) { + aTask->Run(); + return true; + } + + AsyncRun(aTask); + return false; +} + +void DecodePool::SyncRunIfPossible(IDecodingTask* aTask, + const nsCString& aURI) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTask); + + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("DecodePool::SyncRunIfPossible", + GRAPHICS, aURI); + + aTask->Run(); +} + +already_AddRefed DecodePool::GetIOEventTarget() { + MutexAutoLock threadPoolLock(mMutex); + nsCOMPtr target = mIOThread; + return target.forget(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/DecodePool.h b/image/DecodePool.h new file mode 100644 index 0000000000..6b25aadf17 --- /dev/null +++ b/image/DecodePool.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * DecodePool manages the threads used for decoding raster images. + */ + +#ifndef mozilla_image_DecodePool_h +#define mozilla_image_DecodePool_h + +#include "mozilla/Mutex.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIEventTarget.h" +#include "nsIObserver.h" +#include "mozilla/RefPtr.h" +#include "nsStringFwd.h" + +class nsIThread; +class nsIThreadPool; + +namespace mozilla { +namespace image { + +class Decoder; +class DecodePoolImpl; +class IDecodingTask; + +/** + * DecodePool is a singleton class that manages decoding of raster images. It + * owns a pool of image decoding threads that are used for asynchronous + * decoding. + * + * DecodePool allows callers to run a decoder, handling management of the + * decoder's lifecycle and whether it executes on the main thread, + * off-main-thread in the image decoding thread pool, or on some combination of + * the two. + */ +class DecodePool final : public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + /// Initializes the singleton instance. Should be called from the main thread. + static void Initialize(); + + /// Returns the singleton instance. + static DecodePool* Singleton(); + + /// @return the number of processor cores we have available. This is not the + /// same as the number of decoding threads we're actually using. + static uint32_t NumberOfCores(); + + /// True if the DecodePool is being shutdown. This may only be called by + /// threads from the pool to check if they should keep working or not. + bool IsShuttingDown() const; + + /// Ask the DecodePool to run @aTask asynchronously and return immediately. + void AsyncRun(IDecodingTask* aTask); + + /** + * Run @aTask synchronously if the task would prefer it. It's up to the task + * itself to make this decision; @see IDecodingTask::ShouldPreferSyncRun(). If + * @aTask doesn't prefer it, just run @aTask asynchronously and return + * immediately. + * @return true if the task was run sync, false otherwise. + */ + bool SyncRunIfPreferred(IDecodingTask* aTask, const nsCString& aURI); + + /** + * Run @aTask synchronously. This does not guarantee that @aTask will complete + * synchronously. If, for example, @aTask doesn't yet have the data it needs + * to run synchronously, it may recover by scheduling an async task to finish + * up the work when the remaining data is available. + */ + void SyncRunIfPossible(IDecodingTask* aTask, const nsCString& aURI); + + /** + * Returns an event target interface to the DecodePool's I/O thread. Callers + * who want to deliver data to workers on the DecodePool can use this event + * target. + * + * @return An nsISerialEventTarget interface to the thread pool's I/O thread. + */ + already_AddRefed GetIOEventTarget(); + + private: + friend class DecodePoolWorker; + + DecodePool(); + virtual ~DecodePool(); + + static StaticRefPtr sSingleton; + static uint32_t sNumCores; + bool mShuttingDown = false; + + // mMutex protects mIOThread. + Mutex mMutex; + nsCOMPtr mIOThread MOZ_GUARDED_BY(mMutex); +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_DecodePool_h diff --git a/image/DecodedSurfaceProvider.cpp b/image/DecodedSurfaceProvider.cpp new file mode 100644 index 0000000000..cdfc27bcda --- /dev/null +++ b/image/DecodedSurfaceProvider.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DecodedSurfaceProvider.h" + +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/layers/SharedSurfacesChild.h" +#include "nsProxyRelease.h" + +#include "Decoder.h" + +using namespace mozilla::gfx; +using namespace mozilla::layers; + +namespace mozilla { +namespace image { + +DecodedSurfaceProvider::DecodedSurfaceProvider(NotNull aImage, + const SurfaceKey& aSurfaceKey, + NotNull aDecoder) + : ISurfaceProvider(ImageKey(aImage.get()), aSurfaceKey, + AvailabilityState::StartAsPlaceholder()), + mImage(aImage.get()), + mMutex("mozilla::image::DecodedSurfaceProvider"), + mDecoder(aDecoder.get()) { + MOZ_ASSERT(!mDecoder->IsMetadataDecode(), + "Use MetadataDecodingTask for metadata decodes"); + MOZ_ASSERT(mDecoder->IsFirstFrameDecode(), + "Use AnimationSurfaceProvider for animation decodes"); +} + +DecodedSurfaceProvider::~DecodedSurfaceProvider() { DropImageReference(); } + +void DecodedSurfaceProvider::DropImageReference() { + if (!mImage) { + return; // Nothing to do. + } + + // RasterImage objects need to be destroyed on the main thread. We also need + // to destroy them asynchronously, because if our surface cache entry is + // destroyed and we were the only thing keeping |mImage| alive, RasterImage's + // destructor may call into the surface cache while whatever code caused us to + // get evicted is holding the surface cache lock, causing deadlock. + RefPtr image = mImage; + mImage = nullptr; + SurfaceCache::ReleaseImageOnMainThread(image.forget(), + /* aAlwaysProxy = */ true); +} + +DrawableFrameRef DecodedSurfaceProvider::DrawableRef(size_t aFrame) { + MOZ_ASSERT(aFrame == 0, + "Requesting an animation frame from a DecodedSurfaceProvider?"); + + // We depend on SurfaceCache::SurfaceAvailable() to provide synchronization + // for methods that touch |mSurface|; after SurfaceAvailable() is called, + // |mSurface| should be non-null and shouldn't be mutated further until we get + // destroyed. That means that the assertions below are very important; we'll + // end up with data races if these assumptions are violated. + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling DrawableRef() on a placeholder"); + return DrawableFrameRef(); + } + + if (!mSurface) { + MOZ_ASSERT_UNREACHABLE("Calling DrawableRef() when we have no surface"); + return DrawableFrameRef(); + } + + return mSurface->DrawableRef(); +} + +bool DecodedSurfaceProvider::IsFinished() const { + // See DrawableRef() for commentary on these assertions. + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling IsFinished() on a placeholder"); + return false; + } + + if (!mSurface) { + MOZ_ASSERT_UNREACHABLE("Calling IsFinished() when we have no surface"); + return false; + } + + return mSurface->IsFinished(); +} + +void DecodedSurfaceProvider::SetLocked(bool aLocked) { + // See DrawableRef() for commentary on these assertions. + if (Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling SetLocked() on a placeholder"); + return; + } + + if (!mSurface) { + MOZ_ASSERT_UNREACHABLE("Calling SetLocked() when we have no surface"); + return; + } + + if (aLocked == IsLocked()) { + return; // Nothing to do. + } + + // If we're locked, hold a DrawableFrameRef to |mSurface|, which will keep any + // volatile buffer it owns in memory. + mLockRef = aLocked ? mSurface->DrawableRef() : DrawableFrameRef(); +} + +size_t DecodedSurfaceProvider::LogicalSizeInBytes() const { + // Single frame images are always 32bpp. + IntSize size = GetSurfaceKey().Size(); + return size_t(size.width) * size_t(size.height) * sizeof(uint32_t); +} + +void DecodedSurfaceProvider::Run() { + MutexAutoLock lock(mMutex); + + if (!mDecoder || !mImage) { + MOZ_ASSERT_UNREACHABLE("Running after decoding finished?"); + return; + } + + // Run the decoder. + LexerResult result = mDecoder->Decode(WrapNotNull(this)); + + // If there's a new surface available, announce it to the surface cache. + CheckForNewSurface(); + + if (result.is()) { + FinishDecoding(); + return; // We're done. + } + + // Notify for the progress we've made so far. + if (mDecoder->HasProgress()) { + NotifyProgress(WrapNotNull(mImage), WrapNotNull(mDecoder)); + } + + MOZ_ASSERT(result.is()); + + if (result == LexerResult(Yield::NEED_MORE_DATA)) { + // We can't make any more progress right now. The decoder itself will ensure + // that we get reenqueued when more data is available; just return for now. + return; + } + + // Single-frame images shouldn't yield for any reason except NEED_MORE_DATA. + MOZ_ASSERT_UNREACHABLE("Unexpected yield for single-frame image"); + mDecoder->TerminateFailure(); + FinishDecoding(); +} + +void DecodedSurfaceProvider::CheckForNewSurface() { + mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mDecoder); + + if (mSurface) { + // Single-frame images should produce no more than one surface, so if we + // have one, it should be the same one the decoder is working on. + MOZ_ASSERT(mSurface.get() == mDecoder->GetCurrentFrameRef().get(), + "DecodedSurfaceProvider and Decoder have different surfaces?"); + return; + } + + // We don't have a surface yet; try to get one from the decoder. + mSurface = mDecoder->GetCurrentFrameRef().get(); + if (!mSurface) { + return; // No surface yet. + } + + // We just got a surface for the first time; let the surface cache know. + MOZ_ASSERT(mImage); + SurfaceCache::SurfaceAvailable(WrapNotNull(this)); +} + +void DecodedSurfaceProvider::FinishDecoding() { + mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(mImage); + MOZ_ASSERT(mDecoder); + + // Send notifications. + NotifyDecodeComplete(WrapNotNull(mImage), WrapNotNull(mDecoder)); + + // If we have a new and complete surface, we can try to prune similarly sized + // surfaces if the cache supports it. + if (mSurface && mSurface->IsFinished()) { + SurfaceCache::PruneImage(ImageKey(mImage)); + } + + // Destroy our decoder; we don't need it anymore. (And if we don't destroy it, + // our surface can never be optimized, because the decoder has a + // RawAccessFrameRef to it.) + mDecoder = nullptr; + + // We don't need a reference to our image anymore, either, and we don't want + // one. We may be stored in the surface cache for a long time after decoding + // finishes. If we don't drop our reference to the image, we'll end up + // keeping it alive as long as we remain in the surface cache, which could + // greatly extend the image's lifetime - in fact, if the image isn't + // discardable, it'd result in a leak! + DropImageReference(); +} + +bool DecodedSurfaceProvider::ShouldPreferSyncRun() const { + return mDecoder->ShouldSyncDecode( + StaticPrefs::image_mem_decode_bytes_at_a_time_AtStartup()); +} + +nsresult DecodedSurfaceProvider::UpdateKey( + layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, wr::ImageKey& aKey) { + MOZ_ASSERT(mSurface); + RefPtr surface = mSurface->GetSourceSurface(); + if (!surface) { + return NS_ERROR_FAILURE; + } + + return SharedSurfacesChild::Share(surface, aManager, aResources, aKey); +} + +nsresult SimpleSurfaceProvider::UpdateKey( + layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, wr::ImageKey& aKey) { + RefPtr surface = mSurface->GetSourceSurface(); + if (!surface) { + return NS_ERROR_FAILURE; + } + + return SharedSurfacesChild::Share(surface, aManager, aResources, aKey); +} + +} // namespace image +} // namespace mozilla diff --git a/image/DecodedSurfaceProvider.h b/image/DecodedSurfaceProvider.h new file mode 100644 index 0000000000..07e722daa6 --- /dev/null +++ b/image/DecodedSurfaceProvider.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * An ISurfaceProvider implemented for single-frame decoded surfaces. + */ + +#ifndef mozilla_image_DecodedSurfaceProvider_h +#define mozilla_image_DecodedSurfaceProvider_h + +#include "IDecodingTask.h" +#include "ISurfaceProvider.h" +#include "SurfaceCache.h" + +namespace mozilla { +namespace image { + +/** + * An ISurfaceProvider that manages the decoding of a single-frame image and + * stores the resulting surface. + */ +class DecodedSurfaceProvider final : public ISurfaceProvider, + public IDecodingTask { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecodedSurfaceProvider, override) + + DecodedSurfaceProvider(NotNull aImage, + const SurfaceKey& aSurfaceKey, + NotNull aDecoder); + + ////////////////////////////////////////////////////////////////////////////// + // ISurfaceProvider implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + bool IsFinished() const override; + size_t LogicalSizeInBytes() const override; + + protected: + DrawableFrameRef DrawableRef(size_t aFrame) override; + bool IsLocked() const override { return bool(mLockRef); } + void SetLocked(bool aLocked) override; + + ////////////////////////////////////////////////////////////////////////////// + // IDecodingTask implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + void Run() override; + bool ShouldPreferSyncRun() const override; + + // Full decodes are low priority compared to metadata decodes because they + // don't block layout or page load. + TaskPriority Priority() const override { return TaskPriority::eLow; } + + ////////////////////////////////////////////////////////////////////////////// + // WebRenderImageProvider implementation. + ////////////////////////////////////////////////////////////////////////////// + + public: + nsresult UpdateKey(layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + wr::ImageKey& aKey) override; + + private: + virtual ~DecodedSurfaceProvider(); + + void DropImageReference(); + void CheckForNewSurface(); + void FinishDecoding(); + + /// The image associated with our decoder. Dropped after decoding. + RefPtr mImage; + + /// Mutex protecting access to mDecoder. + Mutex mMutex MOZ_UNANNOTATED; + + /// The decoder that will generate our surface. Dropped after decoding. + RefPtr mDecoder; + + /// Our surface. Initially null until it's generated by the decoder. + RefPtr mSurface; + + /// A drawable reference to our service; used for locking. + DrawableFrameRef mLockRef; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_DecodedSurfaceProvider_h diff --git a/image/Decoder.cpp b/image/Decoder.cpp new file mode 100644 index 0000000000..76301d7596 --- /dev/null +++ b/image/Decoder.cpp @@ -0,0 +1,570 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Decoder.h" + +#include "DecodePool.h" +#include "IDecodingTask.h" +#include "ISurfaceProvider.h" +#include "gfxPlatform.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/Telemetry.h" +#include "nsComponentManagerUtils.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" + +using mozilla::gfx::IntPoint; +using mozilla::gfx::IntRect; +using mozilla::gfx::IntSize; +using mozilla::gfx::SurfaceFormat; + +namespace mozilla { +namespace image { + +class MOZ_STACK_CLASS AutoRecordDecoderTelemetry final { + public: + explicit AutoRecordDecoderTelemetry(Decoder* aDecoder) : mDecoder(aDecoder) { + MOZ_ASSERT(mDecoder); + + // Begin recording telemetry data. + mStartTime = TimeStamp::Now(); + } + + ~AutoRecordDecoderTelemetry() { + // Finish telemetry. + mDecoder->mDecodeTime += (TimeStamp::Now() - mStartTime); + } + + private: + Decoder* mDecoder; + TimeStamp mStartTime; +}; + +Decoder::Decoder(RasterImage* aImage) + : mInProfile(nullptr), + mTransform(nullptr), + mImageData(nullptr), + mImageDataLength(0), + mCMSMode(gfxPlatform::GetCMSMode()), + mImage(aImage), + mFrameRecycler(nullptr), + mProgress(NoProgress), + mFrameCount(0), + mLoopLength(FrameTimeout::Zero()), + mDecoderFlags(DefaultDecoderFlags()), + mSurfaceFlags(DefaultSurfaceFlags()), + mInitialized(false), + mMetadataDecode(false), + mHaveExplicitOutputSize(false), + mInFrame(false), + mFinishedNewFrame(false), + mHasFrameToTake(false), + mReachedTerminalState(false), + mDecodeDone(false), + mError(false), + mShouldReportError(false), + mFinalizeFrames(true) {} + +Decoder::~Decoder() { + MOZ_ASSERT(mProgress == NoProgress || !mImage, + "Destroying Decoder without taking all its progress changes"); + MOZ_ASSERT(mInvalidRect.IsEmpty() || !mImage, + "Destroying Decoder without taking all its invalidations"); + mInitialized = false; + + if (mInProfile) { + // mTransform belongs to us only if mInProfile is non-null + if (mTransform) { + qcms_transform_release(mTransform); + } + qcms_profile_release(mInProfile); + } + + if (mImage && !NS_IsMainThread()) { + // Dispatch mImage to main thread to prevent it from being destructed by the + // decode thread. + SurfaceCache::ReleaseImageOnMainThread(mImage.forget()); + } +} + +void Decoder::SetSurfaceFlags(SurfaceFlags aSurfaceFlags) { + MOZ_ASSERT(!mInitialized); + mSurfaceFlags = aSurfaceFlags; + if (mSurfaceFlags & SurfaceFlags::NO_COLORSPACE_CONVERSION) { + mCMSMode = CMSMode::Off; + } +} + +qcms_profile* Decoder::GetCMSOutputProfile() const { + if (mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) { + return gfxPlatform::GetCMSsRGBProfile(); + } + return gfxPlatform::GetCMSOutputProfile(); +} + +qcms_transform* Decoder::GetCMSsRGBTransform(SurfaceFormat aFormat) const { + if (mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) { + // We want a transform to convert from sRGB to device space, but we are + // already using sRGB as our device space. That means we can skip + // color management entirely. + return nullptr; + } + if (qcms_profile_is_sRGB(gfxPlatform::GetCMSOutputProfile())) { + // Device space is sRGB so we can skip color management as well. + return nullptr; + } + + switch (aFormat) { + case SurfaceFormat::B8G8R8A8: + case SurfaceFormat::B8G8R8X8: + return gfxPlatform::GetCMSBGRATransform(); + case SurfaceFormat::R8G8B8A8: + case SurfaceFormat::R8G8B8X8: + return gfxPlatform::GetCMSRGBATransform(); + case SurfaceFormat::R8G8B8: + return gfxPlatform::GetCMSRGBTransform(); + default: + MOZ_ASSERT_UNREACHABLE("Unsupported surface format!"); + return nullptr; + } +} + +/* + * Common implementation of the decoder interface. + */ + +nsresult Decoder::Init() { + // No re-initializing + MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!"); + + // All decoders must have a SourceBufferIterator. + MOZ_ASSERT(mIterator); + + // Metadata decoders must not set an output size. + MOZ_ASSERT_IF(mMetadataDecode, !mHaveExplicitOutputSize); + + // All decoders must be anonymous except for metadata decoders. + // XXX(seth): Soon that exception will be removed. + MOZ_ASSERT_IF(mImage, IsMetadataDecode()); + + // Implementation-specific initialization. + nsresult rv = InitInternal(); + + mInitialized = true; + + return rv; +} + +LexerResult Decoder::Decode(IResumable* aOnResume /* = nullptr */) { + MOZ_ASSERT(mInitialized, "Should be initialized here"); + MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); + + // If we're already done, don't attempt to keep decoding. + if (GetDecodeDone()) { + return LexerResult(HasError() ? TerminalState::FAILURE + : TerminalState::SUCCESS); + } + + LexerResult lexerResult(TerminalState::FAILURE); + { + AUTO_PROFILER_LABEL_CATEGORY_PAIR_RELEVANT_FOR_JS(GRAPHICS_ImageDecoding); + AutoRecordDecoderTelemetry telemetry(this); + + lexerResult = DoDecode(*mIterator, aOnResume); + }; + + if (lexerResult.is()) { + // We either need more data to continue (in which case either @aOnResume or + // the caller will reschedule us to run again later), or the decoder is + // yielding to allow the caller access to some intermediate output. + return lexerResult; + } + + // We reached a terminal state; we're now done decoding. + MOZ_ASSERT(lexerResult.is()); + mReachedTerminalState = true; + + // If decoding failed, record that fact. + if (lexerResult.as() == TerminalState::FAILURE) { + PostError(); + } + + // Perform final cleanup. + CompleteDecode(); + + return LexerResult(HasError() ? TerminalState::FAILURE + : TerminalState::SUCCESS); +} + +LexerResult Decoder::TerminateFailure() { + PostError(); + + // Perform final cleanup if need be. + if (!mReachedTerminalState) { + mReachedTerminalState = true; + CompleteDecode(); + } + + return LexerResult(TerminalState::FAILURE); +} + +bool Decoder::ShouldSyncDecode(size_t aByteLimit) { + MOZ_ASSERT(aByteLimit > 0); + MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); + + return mIterator->RemainingBytesIsNoMoreThan(aByteLimit); +} + +void Decoder::CompleteDecode() { + // Implementation-specific finalization. + nsresult rv = BeforeFinishInternal(); + if (NS_FAILED(rv)) { + PostError(); + } + + rv = HasError() ? FinishWithErrorInternal() : FinishInternal(); + if (NS_FAILED(rv)) { + PostError(); + } + + if (IsMetadataDecode()) { + // If this was a metadata decode and we never got a size, the decode failed. + if (!HasSize()) { + PostError(); + } + return; + } + + // If the implementation left us mid-frame, finish that up. Note that it may + // have left us transparent. + if (mInFrame) { + PostHasTransparency(); + PostFrameStop(); + } + + // If PostDecodeDone() has not been called, we may need to send teardown + // notifications if it is unrecoverable. + if (mDecodeDone) { + MOZ_ASSERT(HasError() || mCurrentFrame, "Should have an error or a frame"); + } else { + // We should always report an error to the console in this case. + mShouldReportError = true; + + if (GetCompleteFrameCount() > 0) { + // We're usable if we have at least one complete frame, so do exactly + // what we should have when the decoder completed. + PostHasTransparency(); + PostDecodeDone(); + } else { + // We're not usable. Record some final progress indicating the error. + mProgress |= FLAG_DECODE_COMPLETE | FLAG_HAS_ERROR; + } + } +} + +void Decoder::SetOutputSize(const OrientedIntSize& aSize) { + mOutputSize = Some(aSize); + mHaveExplicitOutputSize = true; +} + +Maybe Decoder::ExplicitOutputSize() const { + MOZ_ASSERT_IF(mHaveExplicitOutputSize, mOutputSize); + return mHaveExplicitOutputSize ? mOutputSize : Nothing(); +} + +Maybe Decoder::TakeCompleteFrameCount() { + const bool finishedNewFrame = mFinishedNewFrame; + mFinishedNewFrame = false; + return finishedNewFrame ? Some(GetCompleteFrameCount()) : Nothing(); +} + +DecoderFinalStatus Decoder::FinalStatus() const { + return DecoderFinalStatus(IsMetadataDecode(), GetDecodeDone(), HasError(), + ShouldReportError()); +} + +DecoderTelemetry Decoder::Telemetry() const { + MOZ_ASSERT(mIterator); + return DecoderTelemetry(SpeedHistogram(), + mIterator ? mIterator->ByteCount() : 0, + mIterator ? mIterator->ChunkCount() : 0, mDecodeTime); +} + +nsresult Decoder::AllocateFrame(const gfx::IntSize& aOutputSize, + gfx::SurfaceFormat aFormat, + const Maybe& aAnimParams) { + mCurrentFrame = AllocateFrameInternal(aOutputSize, aFormat, aAnimParams, + std::move(mCurrentFrame)); + + if (mCurrentFrame) { + mHasFrameToTake = true; + + // Gather the raw pointers the decoders will use. + mCurrentFrame->GetImageData(&mImageData, &mImageDataLength); + + // We should now be on |aFrameNum|. (Note that we're comparing the frame + // number, which is zero-based, with the frame count, which is one-based.) + MOZ_ASSERT_IF(aAnimParams, aAnimParams->mFrameNum + 1 == mFrameCount); + + // If we're past the first frame, PostIsAnimated() should've been called. + MOZ_ASSERT_IF(mFrameCount > 1, HasAnimation()); + + // Update our state to reflect the new frame. + MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!"); + mInFrame = true; + } + + return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE; +} + +RawAccessFrameRef Decoder::AllocateFrameInternal( + const gfx::IntSize& aOutputSize, SurfaceFormat aFormat, + const Maybe& aAnimParams, + RawAccessFrameRef&& aPreviousFrame) { + if (HasError()) { + return RawAccessFrameRef(); + } + + uint32_t frameNum = aAnimParams ? aAnimParams->mFrameNum : 0; + if (frameNum != mFrameCount) { + MOZ_ASSERT_UNREACHABLE("Allocating frames out of order"); + return RawAccessFrameRef(); + } + + if (aOutputSize.width <= 0 || aOutputSize.height <= 0) { + NS_WARNING("Trying to add frame with zero or negative size"); + return RawAccessFrameRef(); + } + + if (frameNum > 0) { + if (aPreviousFrame->GetDisposalMethod() != + DisposalMethod::RESTORE_PREVIOUS) { + // If the new restore frame is the direct previous frame, then we know + // the dirty rect is composed only of the current frame's blend rect and + // the restore frame's clear rect (if applicable) which are handled in + // filters. + mRestoreFrame = std::move(aPreviousFrame); + mRestoreDirtyRect.SetBox(0, 0, 0, 0); + } else { + // We only need the previous frame's dirty rect, because while there may + // have been several frames between us and mRestoreFrame, the only areas + // that changed are the restore frame's clear rect, the current frame + // blending rect, and the previous frame's blending rect. All else is + // forgotten due to us restoring the same frame again. + mRestoreDirtyRect = aPreviousFrame->GetBoundedBlendRect(); + } + } + + RawAccessFrameRef ref; + + // If we have a frame recycler, it must be for an animated image producing + // full frames. If the higher layers are discarding frames because of the + // memory footprint, then the recycler will allow us to reuse the buffers. + // Each frame should be the same size and have mostly the same properties. + if (mFrameRecycler) { + MOZ_ASSERT(aAnimParams); + + ref = mFrameRecycler->RecycleFrame(mRecycleRect); + if (ref) { + // If the recycled frame is actually the current restore frame, we cannot + // use it. If the next restore frame is the new frame we are creating, in + // theory we could reuse it, but we would need to store the restore frame + // animation parameters elsewhere. For now we just drop it. + bool blocked = ref.get() == mRestoreFrame.get(); + if (!blocked) { + blocked = NS_FAILED(ref->InitForDecoderRecycle(aAnimParams.ref())); + } + + if (blocked) { + ref.reset(); + } + } + } + + // Either the recycler had nothing to give us, or we don't have a recycler. + // Produce a new frame to store the data. + if (!ref) { + // There is no underlying data to reuse, so reset the recycle rect to be + // the full frame, to ensure the restore frame is fully copied. + mRecycleRect = IntRect(IntPoint(0, 0), aOutputSize); + + bool nonPremult = bool(mSurfaceFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA); + auto frame = MakeNotNull>(); + if (NS_FAILED(frame->InitForDecoder(aOutputSize, aFormat, nonPremult, + aAnimParams, bool(mFrameRecycler)))) { + NS_WARNING("imgFrame::Init should succeed"); + return RawAccessFrameRef(); + } + + ref = frame->RawAccessRef(); + if (!ref) { + frame->Abort(); + return RawAccessFrameRef(); + } + } + + mFrameCount++; + + return ref; +} + +/* + * Hook stubs. Override these as necessary in decoder implementations. + */ + +nsresult Decoder::InitInternal() { return NS_OK; } +nsresult Decoder::BeforeFinishInternal() { return NS_OK; } +nsresult Decoder::FinishInternal() { return NS_OK; } + +nsresult Decoder::FinishWithErrorInternal() { + MOZ_ASSERT(!mInFrame); + return NS_OK; +} + +/* + * Progress Notifications + */ + +void Decoder::PostSize(int32_t aWidth, int32_t aHeight, + Orientation aOrientation, Resolution aResolution) { + // Validate. + MOZ_ASSERT(aWidth >= 0, "Width can't be negative!"); + MOZ_ASSERT(aHeight >= 0, "Height can't be negative!"); + + // Set our intrinsic size. + mImageMetadata.SetSize(aWidth, aHeight, aOrientation, aResolution); + + // Verify it is the expected size, if given. Note that this is only used by + // the ICO decoder for embedded image types, so only its subdecoders are + // required to handle failures in PostSize. + if (!IsExpectedSize()) { + PostError(); + return; + } + + // Set our output size if it's not already set. + if (!mOutputSize) { + mOutputSize = Some(mImageMetadata.GetSize()); + } + + MOZ_ASSERT(mOutputSize->width <= mImageMetadata.GetSize().width && + mOutputSize->height <= mImageMetadata.GetSize().height, + "Output size will result in upscaling"); + + // Record this notification. + mProgress |= FLAG_SIZE_AVAILABLE; +} + +void Decoder::PostHasTransparency() { mProgress |= FLAG_HAS_TRANSPARENCY; } + +void Decoder::PostIsAnimated(FrameTimeout aFirstFrameTimeout) { + mProgress |= FLAG_IS_ANIMATED; + mImageMetadata.SetHasAnimation(); + mImageMetadata.SetFirstFrameTimeout(aFirstFrameTimeout); +} + +void Decoder::PostFrameStop(Opacity aFrameOpacity) { + // We should be mid-frame + MOZ_ASSERT(!IsMetadataDecode(), "Stopping frame during metadata decode"); + MOZ_ASSERT(mInFrame, "Stopping frame when we didn't start one"); + MOZ_ASSERT(mCurrentFrame, "Stopping frame when we don't have one"); + + // Update our state. + mInFrame = false; + mFinishedNewFrame = true; + + mCurrentFrame->Finish( + aFrameOpacity, mFinalizeFrames, + /* aOrientationSwapsWidthAndHeight = */ mImageMetadata.HasOrientation() && + mImageMetadata.GetOrientation().SwapsWidthAndHeight()); + + mProgress |= FLAG_FRAME_COMPLETE; + + mLoopLength += mCurrentFrame->GetTimeout(); + + if (mFrameCount == 1) { + // If we're not sending partial invalidations, then we send an invalidation + // here when the first frame is complete. + if (!ShouldSendPartialInvalidations()) { + mInvalidRect.UnionRect(mInvalidRect, + OrientedIntRect(OrientedIntPoint(), Size())); + } + + // If we dispose of the first frame by clearing it, then the first frame's + // refresh area is all of itself. RESTORE_PREVIOUS is invalid (assumed to + // be DISPOSE_CLEAR). + switch (mCurrentFrame->GetDisposalMethod()) { + default: + MOZ_FALLTHROUGH_ASSERT("Unexpected DisposalMethod"); + case DisposalMethod::CLEAR: + case DisposalMethod::CLEAR_ALL: + case DisposalMethod::RESTORE_PREVIOUS: + mFirstFrameRefreshArea = IntRect(IntPoint(), Size().ToUnknownSize()); + break; + case DisposalMethod::KEEP: + case DisposalMethod::NOT_SPECIFIED: + break; + } + } else { + // Some GIFs are huge but only have a small area that they animate. We only + // need to refresh that small area when frame 0 comes around again. + mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, + mCurrentFrame->GetBoundedBlendRect()); + } +} + +void Decoder::PostInvalidation(const OrientedIntRect& aRect, + const Maybe& aRectAtOutputSize + /* = Nothing() */) { + // We should be mid-frame + MOZ_ASSERT(mInFrame, "Can't invalidate when not mid-frame!"); + MOZ_ASSERT(mCurrentFrame, "Can't invalidate when not mid-frame!"); + + // Record this invalidation, unless we're not sending partial invalidations + // or we're past the first frame. + if (ShouldSendPartialInvalidations() && mFrameCount == 1) { + mInvalidRect.UnionRect(mInvalidRect, aRect); + mCurrentFrame->ImageUpdated( + aRectAtOutputSize.valueOr(aRect).ToUnknownRect()); + } +} + +void Decoder::PostDecodeDone(int32_t aLoopCount /* = 0 */) { + MOZ_ASSERT(!IsMetadataDecode(), "Done with decoding in metadata decode"); + MOZ_ASSERT(!mInFrame, "Can't be done decoding if we're mid-frame!"); + MOZ_ASSERT(!mDecodeDone, "Decode already done!"); + mDecodeDone = true; + + mImageMetadata.SetLoopCount(aLoopCount); + + // Some metadata that we track should take into account every frame in the + // image. If this is a first-frame-only decode, our accumulated loop length + // and first frame refresh area only includes the first frame, so it's not + // correct and we don't record it. + if (!IsFirstFrameDecode()) { + mImageMetadata.SetLoopLength(mLoopLength); + mImageMetadata.SetFirstFrameRefreshArea(mFirstFrameRefreshArea); + } + + mProgress |= FLAG_DECODE_COMPLETE; +} + +void Decoder::PostError() { + mError = true; + + if (mInFrame) { + MOZ_ASSERT(mCurrentFrame); + MOZ_ASSERT(mFrameCount > 0); + mCurrentFrame->Abort(); + mInFrame = false; + --mFrameCount; + mHasFrameToTake = false; + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/Decoder.h b/image/Decoder.h new file mode 100644 index 0000000000..afb1236880 --- /dev/null +++ b/image/Decoder.h @@ -0,0 +1,641 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_Decoder_h +#define mozilla_image_Decoder_h + +#include "FrameAnimator.h" +#include "RasterImage.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "AnimationParams.h" +#include "DecoderFlags.h" +#include "ImageMetadata.h" +#include "Orientation.h" +#include "Resolution.h" +#include "SourceBuffer.h" +#include "StreamingLexer.h" +#include "SurfaceFlags.h" +#include "qcms.h" + +enum class CMSMode : int32_t; + +namespace mozilla { + +namespace Telemetry { +enum HistogramID : uint32_t; +} // namespace Telemetry + +namespace image { + +class imgFrame; + +struct DecoderFinalStatus final { + DecoderFinalStatus(bool aWasMetadataDecode, bool aFinished, bool aHadError, + bool aShouldReportError) + : mWasMetadataDecode(aWasMetadataDecode), + mFinished(aFinished), + mHadError(aHadError), + mShouldReportError(aShouldReportError) {} + + /// True if this was a metadata decode. + const bool mWasMetadataDecode : 1; + + /// True if this decoder finished, whether successfully or due to failure. + const bool mFinished : 1; + + /// True if this decoder encountered an error. + const bool mHadError : 1; + + /// True if this decoder encountered the kind of error that should be reported + /// to the console. + const bool mShouldReportError : 1; +}; + +struct DecoderTelemetry final { + DecoderTelemetry(const Maybe& aSpeedHistogram, + size_t aBytesDecoded, uint32_t aChunkCount, + TimeDuration aDecodeTime) + : mSpeedHistogram(aSpeedHistogram), + mBytesDecoded(aBytesDecoded), + mChunkCount(aChunkCount), + mDecodeTime(aDecodeTime) {} + + /// @return our decoder's speed, in KBps. + int32_t Speed() const { + return mBytesDecoded / (1024 * mDecodeTime.ToSeconds()); + } + + /// @return our decoder's decode time, in microseconds. + int32_t DecodeTimeMicros() { return mDecodeTime.ToMicroseconds(); } + + /// The per-image-format telemetry ID for recording our decoder's speed, or + /// Nothing() if we don't record speed telemetry for this kind of decoder. + const Maybe mSpeedHistogram; + + /// The number of bytes of input our decoder processed. + const size_t mBytesDecoded; + + /// The number of chunks our decoder's input was divided into. + const uint32_t mChunkCount; + + /// The amount of time our decoder spent inside DoDecode(). + const TimeDuration mDecodeTime; +}; + +/** + * Interface which owners of an animated Decoder object must implement in order + * to use recycling. It allows the decoder to get a handle to the recycled + * frames. + */ +class IDecoderFrameRecycler { + public: + /** + * Request the next available recycled imgFrame from the recycler. + * + * @param aRecycleRect If a frame is returned, this must be set to the + * accumulated dirty rect between the frame being + * recycled, and the frame being generated. + * + * @returns The recycled frame, if any is available. + */ + virtual RawAccessFrameRef RecycleFrame(gfx::IntRect& aRecycleRect) = 0; +}; + +class Decoder { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Decoder) + + explicit Decoder(RasterImage* aImage); + + /** + * Initialize an image decoder. Decoders may not be re-initialized. + * + * @return NS_OK if the decoder could be initialized successfully. + */ + nsresult Init(); + + /** + * Decodes, reading all data currently available in the SourceBuffer. + * + * If more data is needed and @aOnResume is non-null, Decode() will schedule + * @aOnResume to be called when more data is available. + * + * @return a LexerResult which may indicate: + * - the image has been successfully decoded (TerminalState::SUCCESS), or + * - the image has failed to decode (TerminalState::FAILURE), or + * - the decoder is yielding until it gets more data + * (Yield::NEED_MORE_DATA), or + * - the decoder is yielding to allow the caller to access intermediate + * output (Yield::OUTPUT_AVAILABLE). + */ + LexerResult Decode(IResumable* aOnResume = nullptr); + + /** + * Terminate this decoder in a failure state, just as if the decoder + * implementation had returned TerminalState::FAILURE from DoDecode(). + * + * XXX(seth): This method should be removed ASAP; it exists only because + * RasterImage::FinalizeDecoder() requires an actual Decoder object as an + * argument, so we can't simply tell RasterImage a decode failed except via an + * intervening decoder. We'll fix this in bug 1291071. + */ + LexerResult TerminateFailure(); + + /** + * Given a maximum number of bytes we're willing to decode, @aByteLimit, + * returns true if we should attempt to run this decoder synchronously. + */ + bool ShouldSyncDecode(size_t aByteLimit); + + /** + * Gets the invalidation region accumulated by the decoder so far, and clears + * the decoder's invalidation region. This means that each call to + * TakeInvalidRect() returns only the invalidation region accumulated since + * the last call to TakeInvalidRect(). + */ + OrientedIntRect TakeInvalidRect() { + OrientedIntRect invalidRect = mInvalidRect; + mInvalidRect.SetEmpty(); + return invalidRect; + } + + /** + * Gets the progress changes accumulated by the decoder so far, and clears + * them. This means that each call to TakeProgress() returns only the changes + * accumulated since the last call to TakeProgress(). + */ + Progress TakeProgress() { + Progress progress = mProgress; + mProgress = NoProgress; + return progress; + } + + /** + * Returns true if there's any progress to report. + */ + bool HasProgress() const { + return mProgress != NoProgress || !mInvalidRect.IsEmpty() || + mFinishedNewFrame; + } + + /* + * State. + */ + + /** + * If we're doing a metadata decode, we only decode the image's headers, which + * is enough to determine the image's intrinsic size. A metadata decode is + * enabled by calling SetMetadataDecode() *before* calling Init(). + */ + void SetMetadataDecode(bool aMetadataDecode) { + MOZ_ASSERT(!mInitialized, "Shouldn't be initialized yet"); + mMetadataDecode = aMetadataDecode; + } + bool IsMetadataDecode() const { return mMetadataDecode; } + + /** + * Sets the output size of this decoder. If this is smaller than the intrinsic + * size of the image, we'll downscale it while decoding. For memory usage + * reasons, upscaling is forbidden and will trigger assertions in debug + * builds. + * + * Not calling SetOutputSize() means that we should just decode at the + * intrinsic size, whatever it is. + * + * If SetOutputSize() was called, ExplicitOutputSize() can be used to + * determine the value that was passed to it. + * + * This must be called before Init() is called. + */ + void SetOutputSize(const OrientedIntSize& aSize); + + /** + * @return the output size of this decoder. If this is smaller than the + * intrinsic size, then the image will be downscaled during the decoding + * process. + * + * Illegal to call if HasSize() returns false. + */ + OrientedIntSize OutputSize() const { + MOZ_ASSERT(HasSize()); + return *mOutputSize; + } + + /** + * @return either the size passed to SetOutputSize() or Nothing(), indicating + * that SetOutputSize() was not explicitly called. + */ + Maybe ExplicitOutputSize() const; + + /** + * Sets the expected image size of this decoder. Decoding will fail if this + * does not match. + */ + void SetExpectedSize(const OrientedIntSize& aSize) { + mExpectedSize.emplace(aSize); + } + + /** + * Is the image size what was expected, if specified? + */ + bool IsExpectedSize() const { + return mExpectedSize.isNothing() || *mExpectedSize == Size(); + } + + /** + * Set an iterator to the SourceBuffer which will feed data to this decoder. + * This must always be called before calling Init(). (And only before Init().) + * + * XXX(seth): We should eliminate this method and pass a SourceBufferIterator + * to the various decoder constructors instead. + */ + void SetIterator(SourceBufferIterator&& aIterator) { + MOZ_ASSERT(!mInitialized, "Shouldn't be initialized yet"); + mIterator.emplace(std::move(aIterator)); + } + + SourceBuffer* GetSourceBuffer() const { return mIterator->Owner(); } + + /** + * Should this decoder send partial invalidations? + */ + bool ShouldSendPartialInvalidations() const { + return !(mDecoderFlags & DecoderFlags::IS_REDECODE); + } + + /** + * Should we stop decoding after the first frame? + */ + bool IsFirstFrameDecode() const { + return bool(mDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY); + } + + /** + * @return the number of complete animation frames which have been decoded so + * far, if it has changed since the last call to TakeCompleteFrameCount(); + * otherwise, returns Nothing(). + */ + Maybe TakeCompleteFrameCount(); + + // The number of frames we have, including anything in-progress. Thus, this + // is only 0 if we haven't begun any frames. + uint32_t GetFrameCount() { return mFrameCount; } + + // Did we discover that the image we're decoding is animated? + bool HasAnimation() const { return mImageMetadata.HasAnimation(); } + + // Error tracking + bool HasError() const { return mError; } + bool ShouldReportError() const { return mShouldReportError; } + + // Finalize frames + void SetFinalizeFrames(bool aFinalize) { mFinalizeFrames = aFinalize; } + bool GetFinalizeFrames() const { return mFinalizeFrames; } + + /// Did we finish decoding enough that calling Decode() again would be + /// useless? + bool GetDecodeDone() const { + return mReachedTerminalState || mDecodeDone || + (mMetadataDecode && HasSize()) || HasError(); + } + + /// Are we in the middle of a frame right now? Used for assertions only. + bool InFrame() const { return mInFrame; } + + /// Is the image valid if embedded inside an ICO. + virtual bool IsValidICOResource() const { return false; } + + /// Type of decoder. + virtual DecoderType GetType() const { return DecoderType::UNKNOWN; } + + enum DecodeStyle { + PROGRESSIVE, // produce intermediate frames representing the partial + // state of the image + SEQUENTIAL // decode to final image immediately + }; + + /** + * Get or set the DecoderFlags that influence the behavior of this decoder. + */ + void SetDecoderFlags(DecoderFlags aDecoderFlags) { + MOZ_ASSERT(!mInitialized); + mDecoderFlags = aDecoderFlags; + } + DecoderFlags GetDecoderFlags() const { return mDecoderFlags; } + + /** + * Get or set the SurfaceFlags that select the kind of output this decoder + * will produce. + */ + void SetSurfaceFlags(SurfaceFlags aSurfaceFlags); + SurfaceFlags GetSurfaceFlags() const { return mSurfaceFlags; } + + /// @return true if we know the intrinsic size of the image we're decoding. + bool HasSize() const { return mImageMetadata.HasSize(); } + + /** + * @return the intrinsic size of the image we're decoding. + * + * Illegal to call if HasSize() returns false. + */ + OrientedIntSize Size() const { + MOZ_ASSERT(HasSize()); + return mImageMetadata.GetSize(); + } + + /** + * @return an IntRect which covers the entire area of this image at its + * intrinsic size, appropriate for use as a frame rect when the image itself + * does not specify one. + * + * Illegal to call if HasSize() returns false. + */ + OrientedIntRect FullFrame() const { + return OrientedIntRect(OrientedIntPoint(), Size()); + } + + /** + * @return an IntRect which covers the entire area of this image at its size + * after scaling - that is, at its output size. + * + * XXX(seth): This is only used for decoders which are using the old + * Downscaler code instead of SurfacePipe, since the old AllocateFrame() and + * Downscaler APIs required that the frame rect be specified in output space. + * We should remove this once all decoders use SurfacePipe. + * + * Illegal to call if HasSize() returns false. + */ + OrientedIntRect FullOutputFrame() const { + return OrientedIntRect(OrientedIntPoint(), OutputSize()); + } + + /** + * @return the orientation of the image. + * + * Illegal to call if HasSize() returns false. + */ + Orientation GetOrientation() const { + MOZ_ASSERT(HasSize()); + return mImageMetadata.GetOrientation(); + } + + /// @return final status information about this decoder. Should be called + /// after we decide we're not going to run the decoder anymore. + DecoderFinalStatus FinalStatus() const; + + /// @return the metadata we collected about this image while decoding. + const ImageMetadata& GetImageMetadata() { return mImageMetadata; } + + /// @return performance telemetry we collected while decoding. + DecoderTelemetry Telemetry() const; + + /** + * @return a weak pointer to the image associated with this decoder. Illegal + * to call if this decoder is not associated with an image. + */ + NotNull GetImage() const { return WrapNotNull(mImage.get()); } + + /** + * @return a possibly-null weak pointer to the image associated with this + * decoder. May be called even if this decoder is not associated with an + * image. + */ + RasterImage* GetImageMaybeNull() const { return mImage.get(); } + + RawAccessFrameRef GetCurrentFrameRef() { + return mCurrentFrame ? mCurrentFrame->RawAccessRef() : RawAccessFrameRef(); + } + + /** + * For use during decoding only. Allows the BlendAnimationFilter to get the + * current frame we are producing for its animation parameters. + */ + imgFrame* GetCurrentFrame() { return mCurrentFrame.get(); } + + /** + * For use during decoding only. Allows the BlendAnimationFilter to get the + * frame it should be pulling the previous frame data from. + */ + const RawAccessFrameRef& GetRestoreFrameRef() const { return mRestoreFrame; } + + const gfx::IntRect& GetRestoreDirtyRect() const { return mRestoreDirtyRect; } + + const gfx::IntRect& GetRecycleRect() const { return mRecycleRect; } + + const gfx::IntRect& GetFirstFrameRefreshArea() const { + return mFirstFrameRefreshArea; + } + + bool HasFrameToTake() const { return mHasFrameToTake; } + void ClearHasFrameToTake() { + MOZ_ASSERT(mHasFrameToTake); + mHasFrameToTake = false; + } + + IDecoderFrameRecycler* GetFrameRecycler() const { return mFrameRecycler; } + void SetFrameRecycler(IDecoderFrameRecycler* aFrameRecycler) { + mFrameRecycler = aFrameRecycler; + } + + protected: + friend class AutoRecordDecoderTelemetry; + friend class DecoderTestHelper; + friend class nsBMPDecoder; + friend class nsICODecoder; + friend class ReorientSurfaceSink; + friend class SurfaceSink; + + virtual ~Decoder(); + + /* + * Internal hooks. Decoder implementations may override these and + * only these methods. + * + * BeforeFinishInternal() can be used to detect if decoding is in an + * incomplete state, e.g. due to file truncation, in which case it should + * return a failing nsresult. + */ + virtual nsresult InitInternal(); + virtual LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) = 0; + virtual nsresult BeforeFinishInternal(); + virtual nsresult FinishInternal(); + virtual nsresult FinishWithErrorInternal(); + + qcms_profile* GetCMSOutputProfile() const; + qcms_transform* GetCMSsRGBTransform(gfx::SurfaceFormat aFormat) const; + + /** + * @return the per-image-format telemetry ID for recording this decoder's + * speed, or Nothing() if we don't record speed telemetry for this kind of + * decoder. + */ + virtual Maybe SpeedHistogram() const { + return Nothing(); + } + + /* + * Progress notifications. + */ + + // Called by decoders when they determine the size of the image. Informs + // the image of its size and sends notifications. + void PostSize(int32_t aWidth, int32_t aHeight, Orientation = Orientation(), + Resolution = Resolution()); + + // Called by decoders if they determine that the image has transparency. + // + // This should be fired as early as possible to allow observers to do things + // that affect content, so it's necessarily pessimistic - if there's a + // possibility that the image has transparency, for example because its header + // specifies that it has an alpha channel, we fire PostHasTransparency + // immediately. PostFrameStop's aFrameOpacity argument, on the other hand, is + // only used internally to ImageLib. Because PostFrameStop isn't delivered + // until the entire frame has been decoded, decoders may take into account the + // actual contents of the frame and give a more accurate result. + void PostHasTransparency(); + + // Called by decoders if they determine that the image is animated. + // + // @param aTimeout The time for which the first frame should be shown before + // we advance to the next frame. + void PostIsAnimated(FrameTimeout aFirstFrameTimeout); + + // Called by decoders when they end a frame. Informs the image, sends + // notifications, and does internal book-keeping. + // Specify whether this frame is opaque as an optimization. + // For animated images, specify the disposal, blend method and timeout for + // this frame. + void PostFrameStop(Opacity aFrameOpacity = Opacity::SOME_TRANSPARENCY); + + /** + * Called by the decoders when they have a region to invalidate. We may not + * actually pass these invalidations on right away. + * + * @param aRect The invalidation rect in the coordinate system of the unscaled + * image (that is, the image at its intrinsic size). + * @param aRectAtOutputSize If not Nothing(), the invalidation rect in the + * coordinate system of the scaled image (that is, + * the image at our output size). This must + * be supplied if we're downscaling during decode. + */ + void PostInvalidation( + const OrientedIntRect& aRect, + const Maybe& aRectAtOutputSize = Nothing()); + + // Called by the decoders when they have successfully decoded the image. This + // may occur as the result of the decoder getting to the appropriate point in + // the stream, or by us calling FinishInternal(). + // + // May not be called mid-frame. + // + // For animated images, specify the loop count. -1 means loop forever, 0 + // means a single iteration, stopping on the last frame. + void PostDecodeDone(int32_t aLoopCount = 0); + + /** + * Allocates a new frame, making it our current frame if successful. + */ + nsresult AllocateFrame(const gfx::IntSize& aOutputSize, + gfx::SurfaceFormat aFormat, + const Maybe& aAnimParams = Nothing()); + + private: + /// Report that an error was encountered while decoding. + void PostError(); + + /** + * CompleteDecode() finishes up the decoding process after Decode() determines + * that we're finished. It records final progress and does all the cleanup + * that's possible off-main-thread. + */ + void CompleteDecode(); + + /// @return the number of complete frames we have. Does not include the + /// current frame if it's unfinished. + uint32_t GetCompleteFrameCount() { + if (mFrameCount == 0) { + return 0; + } + + return mInFrame ? mFrameCount - 1 : mFrameCount; + } + + RawAccessFrameRef AllocateFrameInternal( + const gfx::IntSize& aOutputSize, gfx::SurfaceFormat aFormat, + const Maybe& aAnimParams, + RawAccessFrameRef&& aPreviousFrame); + + protected: + /// Color management profile from the ICCP chunk in the image. + qcms_profile* mInProfile; + + /// Color management transform to apply to image data. + qcms_transform* mTransform; + + uint8_t* mImageData; // Pointer to image data in BGRA/X + uint32_t mImageDataLength; + + CMSMode mCMSMode; + + private: + RefPtr mImage; + Maybe mIterator; + IDecoderFrameRecycler* mFrameRecycler; + + // The current frame the decoder is producing. + RawAccessFrameRef mCurrentFrame; + + // The complete frame to combine with the current partial frame to produce + // a complete current frame. + RawAccessFrameRef mRestoreFrame; + + ImageMetadata mImageMetadata; + + OrientedIntRect + mInvalidRect; // Tracks new rows as the current frame is decoded. + gfx::IntRect mRestoreDirtyRect; // Tracks an invalidation region between the + // restore frame and the previous frame. + gfx::IntRect mRecycleRect; // Tracks an invalidation region between the + // recycled frame and the current frame. + Maybe mOutputSize; // The size of our output surface. + Maybe mExpectedSize; // The expected size of the image. + Progress mProgress; + + uint32_t mFrameCount; // Number of frames, including anything in-progress + FrameTimeout mLoopLength; // Length of a single loop of this image. + gfx::IntRect + mFirstFrameRefreshArea; // The area of the image that needs to + // be invalidated when the animation loops. + + // Telemetry data for this decoder. + TimeDuration mDecodeTime; + + DecoderFlags mDecoderFlags; + SurfaceFlags mSurfaceFlags; + + bool mInitialized : 1; + bool mMetadataDecode : 1; + bool mHaveExplicitOutputSize : 1; + bool mInFrame : 1; + bool mFinishedNewFrame : 1; // True if PostFrameStop() has been called since + // the last call to TakeCompleteFrameCount(). + // Has a new frame that AnimationSurfaceProvider can take. Unfortunately this + // has to be separate from mFinishedNewFrame because the png decoder yields a + // new frame before calling PostFrameStop(). + bool mHasFrameToTake : 1; + bool mReachedTerminalState : 1; + bool mDecodeDone : 1; + bool mError : 1; + bool mShouldReportError : 1; + bool mFinalizeFrames : 1; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_Decoder_h diff --git a/image/DecoderFactory.cpp b/image/DecoderFactory.cpp new file mode 100644 index 0000000000..4ee7fdfd22 --- /dev/null +++ b/image/DecoderFactory.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DecoderFactory.h" + +#include "nsMimeTypes.h" +#include "mozilla/RefPtr.h" + +#include "AnimationSurfaceProvider.h" +#include "Decoder.h" +#include "DecodedSurfaceProvider.h" +#include "IDecodingTask.h" +#include "ImageOps.h" +#include "nsPNGDecoder.h" +#include "nsGIFDecoder2.h" +#include "nsJPEGDecoder.h" +#include "nsBMPDecoder.h" +#include "nsICODecoder.h" +#include "nsIconDecoder.h" +#include "nsWebPDecoder.h" +#ifdef MOZ_AV1 +# include "nsAVIFDecoder.h" +#endif +#ifdef MOZ_JXL +# include "nsJXLDecoder.h" +#endif + +namespace mozilla { + +using namespace gfx; + +namespace image { + +/* static */ +DecoderType DecoderFactory::GetDecoderType(const char* aMimeType) { + // By default we don't know. + DecoderType type = DecoderType::UNKNOWN; + + // PNG + if (!strcmp(aMimeType, IMAGE_PNG)) { + type = DecoderType::PNG; + } else if (!strcmp(aMimeType, IMAGE_X_PNG)) { + type = DecoderType::PNG; + } else if (!strcmp(aMimeType, IMAGE_APNG)) { + type = DecoderType::PNG; + + // GIF + } else if (!strcmp(aMimeType, IMAGE_GIF)) { + type = DecoderType::GIF; + + // JPEG + } else if (!strcmp(aMimeType, IMAGE_JPEG)) { + type = DecoderType::JPEG; + } else if (!strcmp(aMimeType, IMAGE_PJPEG)) { + type = DecoderType::JPEG; + } else if (!strcmp(aMimeType, IMAGE_JPG)) { + type = DecoderType::JPEG; + + // BMP + } else if (!strcmp(aMimeType, IMAGE_BMP)) { + type = DecoderType::BMP; + } else if (!strcmp(aMimeType, IMAGE_BMP_MS)) { + type = DecoderType::BMP; + + // BMP_CLIPBOARD + } else if (!strcmp(aMimeType, IMAGE_BMP_MS_CLIPBOARD)) { + type = DecoderType::BMP_CLIPBOARD; + + // ICO + } else if (!strcmp(aMimeType, IMAGE_ICO)) { + type = DecoderType::ICO; + } else if (!strcmp(aMimeType, IMAGE_ICO_MS)) { + type = DecoderType::ICO; + + // Icon + } else if (!strcmp(aMimeType, IMAGE_ICON_MS)) { + type = DecoderType::ICON; + + // WebP + } else if (!strcmp(aMimeType, IMAGE_WEBP) && + StaticPrefs::image_webp_enabled()) { + type = DecoderType::WEBP; + + // AVIF + } +#ifdef MOZ_AV1 + else if (!strcmp(aMimeType, IMAGE_AVIF) && + StaticPrefs::image_avif_enabled()) { + type = DecoderType::AVIF; + } +#endif +#ifdef MOZ_JXL + else if (!strcmp(aMimeType, IMAGE_JXL) && StaticPrefs::image_jxl_enabled()) { + type = DecoderType::JXL; + } +#endif + + return type; +} + +/* static */ +DecoderFlags DecoderFactory::GetDefaultDecoderFlagsForType(DecoderType aType) { + auto flags = DefaultDecoderFlags(); + +#ifdef MOZ_AV1 + if (aType == DecoderType::AVIF) { + if (StaticPrefs::image_avif_sequence_enabled()) { + flags |= DecoderFlags::AVIF_SEQUENCES_ENABLED; + } + if (StaticPrefs::image_avif_sequence_animate_avif_major_branded_images()) { + flags |= DecoderFlags::AVIF_ANIMATE_AVIF_MAJOR; + } + } +#endif + + return flags; +} + +/* static */ +already_AddRefed DecoderFactory::GetDecoder(DecoderType aType, + RasterImage* aImage, + bool aIsRedecode) { + RefPtr decoder; + + switch (aType) { + case DecoderType::PNG: + decoder = new nsPNGDecoder(aImage); + break; + case DecoderType::GIF: + decoder = new nsGIFDecoder2(aImage); + break; + case DecoderType::JPEG: + // If we have all the data we don't want to waste cpu time doing + // a progressive decode. + decoder = new nsJPEGDecoder( + aImage, aIsRedecode ? Decoder::SEQUENTIAL : Decoder::PROGRESSIVE); + break; + case DecoderType::BMP: + decoder = new nsBMPDecoder(aImage); + break; + case DecoderType::BMP_CLIPBOARD: + decoder = new nsBMPDecoder(aImage, /* aForClipboard */ true); + break; + case DecoderType::ICO: + decoder = new nsICODecoder(aImage); + break; + case DecoderType::ICON: + decoder = new nsIconDecoder(aImage); + break; + case DecoderType::WEBP: + decoder = new nsWebPDecoder(aImage); + break; +#ifdef MOZ_AV1 + case DecoderType::AVIF: + decoder = new nsAVIFDecoder(aImage); + break; +#endif +#ifdef MOZ_JXL + case DecoderType::JXL: + decoder = new nsJXLDecoder(aImage); + break; +#endif + default: + MOZ_ASSERT_UNREACHABLE("Unknown decoder type"); + } + + return decoder.forget(); +} + +/* static */ +nsresult DecoderFactory::CreateDecoder( + DecoderType aType, NotNull aImage, + NotNull aSourceBuffer, const IntSize& aIntrinsicSize, + const IntSize& aOutputSize, DecoderFlags aDecoderFlags, + SurfaceFlags aSurfaceFlags, IDecodingTask** aOutTask) { + if (aType == DecoderType::UNKNOWN) { + return NS_ERROR_INVALID_ARG; + } + + // Create an anonymous decoder. Interaction with the SurfaceCache and the + // owning RasterImage will be mediated by DecodedSurfaceProvider. + RefPtr decoder = GetDecoder( + aType, nullptr, bool(aDecoderFlags & DecoderFlags::IS_REDECODE)); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(false); + decoder->SetIterator(aSourceBuffer->Iterator()); + decoder->SetOutputSize(OrientedIntSize::FromUnknownSize(aOutputSize)); + decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::FIRST_FRAME_ONLY); + decoder->SetSurfaceFlags(aSurfaceFlags); + + nsresult rv = decoder->Init(); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // Create a DecodedSurfaceProvider which will manage the decoding process and + // make this decoder's output available in the surface cache. + SurfaceKey surfaceKey = + RasterSurfaceKey(aOutputSize, aSurfaceFlags, PlaybackType::eStatic); + auto provider = MakeNotNull>( + aImage, surfaceKey, WrapNotNull(decoder)); + if (aDecoderFlags & DecoderFlags::CANNOT_SUBSTITUTE) { + provider->Availability().SetCannotSubstitute(); + } + + // Attempt to insert the surface provider into the surface cache right away so + // we won't trigger any more decoders with the same parameters. + switch (SurfaceCache::Insert(provider)) { + case InsertOutcome::SUCCESS: + break; + case InsertOutcome::FAILURE_ALREADY_PRESENT: + return NS_ERROR_ALREADY_INITIALIZED; + default: + return NS_ERROR_FAILURE; + } + + // Return the surface provider in its IDecodingTask guise. + RefPtr task = provider.get(); + task.forget(aOutTask); + return NS_OK; +} + +/* static */ +nsresult DecoderFactory::CreateAnimationDecoder( + DecoderType aType, NotNull aImage, + NotNull aSourceBuffer, const IntSize& aIntrinsicSize, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags, + size_t aCurrentFrame, IDecodingTask** aOutTask) { + if (aType == DecoderType::UNKNOWN) { + return NS_ERROR_INVALID_ARG; + } + + MOZ_ASSERT(aType == DecoderType::GIF || aType == DecoderType::PNG || + aType == DecoderType::WEBP || aType == DecoderType::AVIF, + "Calling CreateAnimationDecoder for non-animating DecoderType"); + + // Create an anonymous decoder. Interaction with the SurfaceCache and the + // owning RasterImage will be mediated by AnimationSurfaceProvider. + RefPtr decoder = + GetDecoder(aType, nullptr, /* aIsRedecode = */ true); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(false); + decoder->SetIterator(aSourceBuffer->Iterator()); + decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::IS_REDECODE); + decoder->SetSurfaceFlags(aSurfaceFlags); + + nsresult rv = decoder->Init(); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // Create an AnimationSurfaceProvider which will manage the decoding process + // and make this decoder's output available in the surface cache. + SurfaceKey surfaceKey = + RasterSurfaceKey(aIntrinsicSize, aSurfaceFlags, PlaybackType::eAnimated); + auto provider = MakeNotNull>( + aImage, surfaceKey, WrapNotNull(decoder), aCurrentFrame); + + // Attempt to insert the surface provider into the surface cache right away so + // we won't trigger any more decoders with the same parameters. + switch (SurfaceCache::Insert(provider)) { + case InsertOutcome::SUCCESS: + break; + case InsertOutcome::FAILURE_ALREADY_PRESENT: + return NS_ERROR_ALREADY_INITIALIZED; + default: + return NS_ERROR_FAILURE; + } + + // Return the surface provider in its IDecodingTask guise. + RefPtr task = provider.get(); + task.forget(aOutTask); + return NS_OK; +} + +/* static */ +already_AddRefed DecoderFactory::CloneAnimationDecoder( + Decoder* aDecoder) { + MOZ_ASSERT(aDecoder); + + // In an ideal world, we would assert aDecoder->HasAnimation() but we cannot. + // The decoder may not have detected it is animated yet (e.g. it did not even + // get scheduled yet, or it has only decoded the first frame and has yet to + // rediscover it is animated). + DecoderType type = aDecoder->GetType(); + MOZ_ASSERT(type == DecoderType::GIF || type == DecoderType::PNG || + type == DecoderType::WEBP || type == DecoderType::AVIF, + "Calling CloneAnimationDecoder for non-animating DecoderType"); + + RefPtr decoder = GetDecoder(type, nullptr, /* aIsRedecode = */ true); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(false); + decoder->SetIterator(aDecoder->GetSourceBuffer()->Iterator()); + decoder->SetDecoderFlags(aDecoder->GetDecoderFlags()); + decoder->SetSurfaceFlags(aDecoder->GetSurfaceFlags()); + decoder->SetFrameRecycler(aDecoder->GetFrameRecycler()); + + if (NS_FAILED(decoder->Init())) { + return nullptr; + } + + return decoder.forget(); +} + +/* static */ +already_AddRefed DecoderFactory::CreateMetadataDecoder( + DecoderType aType, NotNull aImage, DecoderFlags aFlags, + NotNull aSourceBuffer) { + if (aType == DecoderType::UNKNOWN) { + return nullptr; + } + + RefPtr decoder = + GetDecoder(aType, aImage, /* aIsRedecode = */ false); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(true); + decoder->SetDecoderFlags(aFlags); + decoder->SetIterator(aSourceBuffer->Iterator()); + + if (NS_FAILED(decoder->Init())) { + return nullptr; + } + + RefPtr task = new MetadataDecodingTask(WrapNotNull(decoder)); + return task.forget(); +} + +/* static */ +already_AddRefed DecoderFactory::CreateDecoderForICOResource( + DecoderType aType, SourceBufferIterator&& aIterator, + NotNull aICODecoder, bool aIsMetadataDecode, + const Maybe& aExpectedSize, + const Maybe& aDataOffset + /* = Nothing() */) { + // Create the decoder. + RefPtr decoder; + switch (aType) { + case DecoderType::BMP: + MOZ_ASSERT(aDataOffset); + decoder = + new nsBMPDecoder(aICODecoder->GetImageMaybeNull(), *aDataOffset); + break; + + case DecoderType::PNG: + MOZ_ASSERT(!aDataOffset); + decoder = new nsPNGDecoder(aICODecoder->GetImageMaybeNull()); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Invalid ICO resource decoder type"); + return nullptr; + } + + MOZ_ASSERT(decoder); + + // Initialize the decoder, copying settings from @aICODecoder. + decoder->SetMetadataDecode(aIsMetadataDecode); + decoder->SetIterator(std::forward(aIterator)); + if (!aIsMetadataDecode) { + decoder->SetOutputSize(aICODecoder->OutputSize()); + } + if (aExpectedSize) { + decoder->SetExpectedSize(*aExpectedSize); + } + decoder->SetDecoderFlags(aICODecoder->GetDecoderFlags()); + decoder->SetSurfaceFlags(aICODecoder->GetSurfaceFlags()); + decoder->SetFinalizeFrames(false); + + if (NS_FAILED(decoder->Init())) { + return nullptr; + } + + return decoder.forget(); +} + +/* static */ +already_AddRefed DecoderFactory::CreateAnonymousDecoder( + DecoderType aType, NotNull aSourceBuffer, + const Maybe& aOutputSize, DecoderFlags aDecoderFlags, + SurfaceFlags aSurfaceFlags) { + if (aType == DecoderType::UNKNOWN) { + return nullptr; + } + + RefPtr decoder = + GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(false); + decoder->SetIterator(aSourceBuffer->Iterator()); + + // Anonymous decoders are always transient; we don't want to optimize surfaces + // or do any other expensive work that might be wasted. + DecoderFlags decoderFlags = DecoderFlags::IMAGE_IS_TRANSIENT; + + decoder->SetDecoderFlags(aDecoderFlags | decoderFlags); + decoder->SetSurfaceFlags(aSurfaceFlags); + + // Set an output size for downscale-during-decode if requested. + if (aOutputSize) { + decoder->SetOutputSize(OrientedIntSize::FromUnknownSize(*aOutputSize)); + } + + if (NS_FAILED(decoder->Init())) { + return nullptr; + } + + return decoder.forget(); +} + +/* static */ +already_AddRefed DecoderFactory::CreateAnonymousMetadataDecoder( + DecoderType aType, NotNull aSourceBuffer) { + if (aType == DecoderType::UNKNOWN) { + return nullptr; + } + + RefPtr decoder = + GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false); + MOZ_ASSERT(decoder, "Should have a decoder now"); + + // Initialize the decoder. + decoder->SetMetadataDecode(true); + decoder->SetIterator(aSourceBuffer->Iterator()); + decoder->SetDecoderFlags(DecoderFlags::FIRST_FRAME_ONLY); + + if (NS_FAILED(decoder->Init())) { + return nullptr; + } + + return decoder.forget(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/DecoderFactory.h b/image/DecoderFactory.h new file mode 100644 index 0000000000..bdf1cce02d --- /dev/null +++ b/image/DecoderFactory.h @@ -0,0 +1,210 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_DecoderFactory_h +#define mozilla_image_DecoderFactory_h + +#include "DecoderFlags.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/gfx/2D.h" +#include "nsCOMPtr.h" +#include "Orientation.h" +#include "SurfaceFlags.h" + +namespace mozilla::image { + +class Decoder; +class IDecodingTask; +class nsICODecoder; +class RasterImage; +class SourceBuffer; +class SourceBufferIterator; + +/** + * The type of decoder; this is usually determined from a MIME type using + * DecoderFactory::GetDecoderType(). + */ +enum class DecoderType { + PNG, + GIF, + JPEG, + BMP, + BMP_CLIPBOARD, + ICO, + ICON, + WEBP, + AVIF, + JXL, + UNKNOWN +}; + +class DecoderFactory { + public: + /// @return the type of decoder which is appropriate for @aMimeType. + static DecoderType GetDecoderType(const char* aMimeType); + + /// @return the default flags to use when creating a decoder of @aType. + static DecoderFlags GetDefaultDecoderFlagsForType(DecoderType aType); + + /** + * Creates and initializes a decoder for non-animated images of type @aType. + * (If the image *is* animated, only the first frame will be decoded.) The + * decoder will send notifications to @aImage. + * + * @param aType Which type of decoder to create - JPEG, PNG, etc. + * @param aImage The image will own the decoder and which should receive + * notifications as decoding progresses. + * @param aSourceBuffer The SourceBuffer which the decoder will read its data + * from. + * @param aIntrinsicSize The intrinsic size of the image, normally obtained + * during the metadata decode. + * @param aOutputSize The output size for the decoder. If this is smaller than + * the intrinsic size, the decoder will downscale the + * image. + * @param aDecoderFlags Flags specifying the behavior of this decoder. + * @param aSurfaceFlags Flags specifying the type of output this decoder + * should produce. + * @param aOutTask Task representing the decoder. + * @return NS_OK if the decoder has been created/initialized successfully; + * NS_ERROR_ALREADY_INITIALIZED if there is already an active decoder + * for this image; + * Else some other unrecoverable error occurred. + */ + static nsresult CreateDecoder(DecoderType aType, NotNull aImage, + NotNull aSourceBuffer, + const gfx::IntSize& aIntrinsicSize, + const gfx::IntSize& aOutputSize, + DecoderFlags aDecoderFlags, + SurfaceFlags aSurfaceFlags, + IDecodingTask** aOutTask); + + /** + * Creates and initializes a decoder for animated images of type @aType. + * The decoder will send notifications to @aImage. + * + * @param aType Which type of decoder to create - JPEG, PNG, etc. + * @param aImage The image will own the decoder and which should receive + * notifications as decoding progresses. + * @param aSourceBuffer The SourceBuffer which the decoder will read its data + * from. + * @param aIntrinsicSize The intrinsic size of the image, normally obtained + * during the metadata decode. + * @param aDecoderFlags Flags specifying the behavior of this decoder. + * @param aSurfaceFlags Flags specifying the type of output this decoder + * should produce. + * @param aCurrentFrame The current frame the decoder should auto advance to. + * @param aOutTask Task representing the decoder. + * @return NS_OK if the decoder has been created/initialized successfully; + * NS_ERROR_ALREADY_INITIALIZED if there is already an active decoder + * for this image; + * Else some other unrecoverable error occurred. + */ + static nsresult CreateAnimationDecoder( + DecoderType aType, NotNull aImage, + NotNull aSourceBuffer, const gfx::IntSize& aIntrinsicSize, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags, + size_t aCurrentFrame, IDecodingTask** aOutTask); + + /** + * Creates and initializes a decoder for animated images, cloned from the + * given decoder. + * + * @param aDecoder Decoder to clone. + */ + static already_AddRefed CloneAnimationDecoder(Decoder* aDecoder); + + /** + * Creates and initializes a metadata decoder of type @aType. This decoder + * will only decode the image's header, extracting metadata like the size of + * the image. No actual image data will be decoded and no surfaces will be + * allocated. The decoder will send notifications to @aImage. + * + * @param aType Which type of decoder to create - JPEG, PNG, etc. + * @param aImage The image will own the decoder and which should receive + * notifications as decoding progresses. + * @param aSourceBuffer The SourceBuffer which the decoder will read its data + * from. + */ + static already_AddRefed CreateMetadataDecoder( + DecoderType aType, NotNull aImage, DecoderFlags aFlags, + NotNull aSourceBuffer); + + /** + * Creates and initializes a decoder for an ICO resource, which may be either + * a BMP or PNG image. + * + * @param aType Which type of decoder to create. This must be either BMP or + * PNG. + * @param aIterator The SourceBufferIterator which the decoder will read its + * data from. + * @param aICODecoder The ICO decoder which is controlling this resource + * decoder. @aICODecoder's settings will be copied to the + * resource decoder, so the two decoders will have the + * same decoder flags, surface flags, target size, and + * other parameters. + * @param aIsMetadataDecode Indicates whether or not this decoder is for + * metadata or not. Independent of the state of the + * parent decoder. + * @param aExpectedSize The expected size of the resource from the ICO header. + * @param aDataOffset If @aType is BMP, specifies the offset at which data + * begins in the BMP resource. Must be Some() if and only + * if @aType is BMP. + */ + static already_AddRefed CreateDecoderForICOResource( + DecoderType aType, SourceBufferIterator&& aIterator, + NotNull aICODecoder, bool aIsMetadataDecode, + const Maybe& aExpectedSize, + const Maybe& aDataOffset = Nothing()); + + /** + * Creates and initializes an anonymous decoder (one which isn't associated + * with an Image object). Only the first frame of the image will be decoded. + * + * @param aType Which type of decoder to create - JPEG, PNG, etc. + * @param aSourceBuffer The SourceBuffer which the decoder will read its data + * from. + * @param aOutputSize If Some(), the output size for the decoder. If this is + * smaller than the intrinsic size, the decoder will + * downscale the image. If Nothing(), the output size will + * be the intrinsic size. + * @param aDecoderFlags Flags specifying the behavior of this decoder. + * @param aSurfaceFlags Flags specifying the type of output this decoder + * should produce. + */ + static already_AddRefed CreateAnonymousDecoder( + DecoderType aType, NotNull aSourceBuffer, + const Maybe& aOutputSize, DecoderFlags aDecoderFlags, + SurfaceFlags aSurfaceFlags); + + /** + * Creates and initializes an anonymous metadata decoder (one which isn't + * associated with an Image object). This decoder will only decode the image's + * header, extracting metadata like the size of the image. No actual image + * data will be decoded and no surfaces will be allocated. + * + * @param aType Which type of decoder to create - JPEG, PNG, etc. + * @param aSourceBuffer The SourceBuffer which the decoder will read its data + * from. + */ + static already_AddRefed CreateAnonymousMetadataDecoder( + DecoderType aType, NotNull aSourceBuffer); + + private: + virtual ~DecoderFactory() = 0; + + /** + * An internal method which allocates a new decoder of the requested @aType. + */ + static already_AddRefed GetDecoder(DecoderType aType, + RasterImage* aImage, + bool aIsRedecode); +}; + +} // namespace mozilla::image + +#endif // mozilla_image_DecoderFactory_h diff --git a/image/DecoderFlags.h b/image/DecoderFlags.h new file mode 100644 index 0000000000..778cdd2942 --- /dev/null +++ b/image/DecoderFlags.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_DecoderFlags_h +#define mozilla_image_DecoderFlags_h + +#include "mozilla/TypedEnumBits.h" + +namespace mozilla { +namespace image { + +/** + * Flags that influence decoder behavior. Note that these flags *don't* + * influence the logical content of the surfaces that the decoder generates, so + * they're not in a factor in SurfaceCache lookups and the like. These flags + * instead either influence which surfaces are generated at all or the tune the + * decoder's behavior for a particular scenario. + */ +enum class DecoderFlags : uint8_t { + FIRST_FRAME_ONLY = 1 << 0, + IS_REDECODE = 1 << 1, + IMAGE_IS_TRANSIENT = 1 << 2, + ASYNC_NOTIFY = 1 << 3, + + /** + * By default, a surface is considered substitutable. That means callers are + * willing to accept a less than ideal match to display. If a caller requires + * a specific size and won't accept alternatives, then this flag should be + * set. + */ + CANNOT_SUBSTITUTE = 1 << 4, + +#ifdef MOZ_AV1 + // The flags below are stored in RasterImage to allow a decoded image to + // remain consistent in whether it is animated or not. + + // Set according to the "image.avif.sequence.enabled" preference. + AVIF_SEQUENCES_ENABLED = 1 << 5, + // Set according to the + // "image.avif.sequence.animate_avif_major_branded_images" preference. + AVIF_ANIMATE_AVIF_MAJOR = 1 << 6, +#endif +}; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DecoderFlags) + +/** + * @return the default set of decode flags. + */ +inline DecoderFlags DefaultDecoderFlags() { return DecoderFlags(); } + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_DecoderFlags_h diff --git a/image/Downscaler.cpp b/image/Downscaler.cpp new file mode 100644 index 0000000000..5bf15f1469 --- /dev/null +++ b/image/Downscaler.cpp @@ -0,0 +1,302 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Downscaler.h" + +#include +#include + +#include "mozilla/gfx/2D.h" + +using std::max; +using std::swap; + +namespace mozilla { + +using gfx::IntRect; + +namespace image { + +Downscaler::Downscaler(const nsIntSize& aTargetSize) + : mTargetSize(aTargetSize), + mOutputBuffer(nullptr), + mWindowCapacity(0), + mLinesInBuffer(0), + mPrevInvalidatedLine(0), + mCurrentOutLine(0), + mCurrentInLine(0), + mHasAlpha(true), + mFlipVertically(false) { + MOZ_ASSERT(mTargetSize.width > 0 && mTargetSize.height > 0, + "Invalid target size"); +} + +Downscaler::~Downscaler() { ReleaseWindow(); } + +void Downscaler::ReleaseWindow() { + if (!mWindow) { + return; + } + + for (int32_t i = 0; i < mWindowCapacity; ++i) { + delete[] mWindow[i]; + } + + mWindow = nullptr; + mWindowCapacity = 0; +} + +nsresult Downscaler::BeginFrame(const nsIntSize& aOriginalSize, + const Maybe& aFrameRect, + uint8_t* aOutputBuffer, bool aHasAlpha, + bool aFlipVertically /* = false */) { + MOZ_ASSERT(aOutputBuffer); + MOZ_ASSERT(mTargetSize != aOriginalSize, + "Created a downscaler, but not downscaling?"); + MOZ_ASSERT(mTargetSize.width <= aOriginalSize.width, + "Created a downscaler, but width is larger"); + MOZ_ASSERT(mTargetSize.height <= aOriginalSize.height, + "Created a downscaler, but height is larger"); + MOZ_ASSERT(aOriginalSize.width > 0 && aOriginalSize.height > 0, + "Invalid original size"); + + // Only downscale from reasonable sizes to avoid using too much memory/cpu + // downscaling and decoding. 1 << 20 == 1,048,576 seems a reasonable limit. + if (aOriginalSize.width > (1 << 20) || aOriginalSize.height > (1 << 20)) { + NS_WARNING("Trying to downscale image frame that is too large"); + return NS_ERROR_INVALID_ARG; + } + + mFrameRect = aFrameRect.valueOr(nsIntRect(nsIntPoint(), aOriginalSize)); + MOZ_ASSERT(mFrameRect.X() >= 0 && mFrameRect.Y() >= 0 && + mFrameRect.Width() >= 0 && mFrameRect.Height() >= 0, + "Frame rect must have non-negative components"); + MOZ_ASSERT(nsIntRect(0, 0, aOriginalSize.width, aOriginalSize.height) + .Contains(mFrameRect), + "Frame rect must fit inside image"); + MOZ_ASSERT_IF(!nsIntRect(0, 0, aOriginalSize.width, aOriginalSize.height) + .IsEqualEdges(mFrameRect), + aHasAlpha); + + mOriginalSize = aOriginalSize; + mScale = gfx::MatrixScalesDouble( + double(mOriginalSize.width) / mTargetSize.width, + double(mOriginalSize.height) / mTargetSize.height); + mOutputBuffer = aOutputBuffer; + mHasAlpha = aHasAlpha; + mFlipVertically = aFlipVertically; + + ReleaseWindow(); + + auto resizeMethod = gfx::ConvolutionFilter::ResizeMethod::LANCZOS3; + if (!mXFilter.ComputeResizeFilter(resizeMethod, mOriginalSize.width, + mTargetSize.width) || + !mYFilter.ComputeResizeFilter(resizeMethod, mOriginalSize.height, + mTargetSize.height)) { + NS_WARNING("Failed to compute filters for image downscaling"); + return NS_ERROR_OUT_OF_MEMORY; + } + + // Allocate the buffer, which contains scanlines of the original image. + // pad to handle overreads by the simd code + size_t bufferLen = gfx::ConvolutionFilter::PadBytesForSIMD( + mOriginalSize.width * sizeof(uint32_t)); + mRowBuffer.reset(new (fallible) uint8_t[bufferLen]); + if (MOZ_UNLIKELY(!mRowBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Zero buffer to keep valgrind happy. + memset(mRowBuffer.get(), 0, bufferLen); + + // Allocate the window, which contains horizontally downscaled scanlines. (We + // can store scanlines which are already downscale because our downscaling + // filter is separable.) + mWindowCapacity = mYFilter.MaxFilter(); + mWindow.reset(new (fallible) uint8_t*[mWindowCapacity]); + if (MOZ_UNLIKELY(!mWindow)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + bool anyAllocationFailed = false; + // pad to handle overreads by the simd code + const size_t rowSize = gfx::ConvolutionFilter::PadBytesForSIMD( + mTargetSize.width * sizeof(uint32_t)); + for (int32_t i = 0; i < mWindowCapacity; ++i) { + mWindow[i] = new (fallible) uint8_t[rowSize]; + anyAllocationFailed = anyAllocationFailed || mWindow[i] == nullptr; + } + + if (MOZ_UNLIKELY(anyAllocationFailed)) { + // We intentionally iterate through the entire array even if an allocation + // fails, to ensure that all the pointers in it are either valid or nullptr. + // That in turn ensures that ReleaseWindow() can clean up correctly. + return NS_ERROR_OUT_OF_MEMORY; + } + + ResetForNextProgressivePass(); + + return NS_OK; +} + +void Downscaler::SkipToRow(int32_t aRow) { + if (mCurrentInLine < aRow) { + ClearRow(); + do { + CommitRow(); + } while (mCurrentInLine < aRow); + } +} + +void Downscaler::ResetForNextProgressivePass() { + mPrevInvalidatedLine = 0; + mCurrentOutLine = 0; + mCurrentInLine = 0; + mLinesInBuffer = 0; + + if (mFrameRect.IsEmpty()) { + // Our frame rect is zero size; commit rows until the end of the image. + SkipToRow(mOriginalSize.height - 1); + } else { + // If we have a vertical offset, commit rows to shift us past it. + SkipToRow(mFrameRect.Y()); + } +} + +void Downscaler::ClearRestOfRow(uint32_t aStartingAtCol) { + MOZ_ASSERT(int64_t(aStartingAtCol) <= int64_t(mOriginalSize.width)); + uint32_t bytesToClear = + (mOriginalSize.width - aStartingAtCol) * sizeof(uint32_t); + memset(mRowBuffer.get() + (aStartingAtCol * sizeof(uint32_t)), 0, + bytesToClear); +} + +void Downscaler::CommitRow() { + MOZ_ASSERT(mOutputBuffer, "Should have a current frame"); + MOZ_ASSERT(mCurrentInLine < mOriginalSize.height, "Past end of input"); + + if (mCurrentOutLine < mTargetSize.height) { + int32_t filterOffset = 0; + int32_t filterLength = 0; + mYFilter.GetFilterOffsetAndLength(mCurrentOutLine, &filterOffset, + &filterLength); + + int32_t inLineToRead = filterOffset + mLinesInBuffer; + MOZ_ASSERT(mCurrentInLine <= inLineToRead, "Reading past end of input"); + if (mCurrentInLine == inLineToRead) { + MOZ_RELEASE_ASSERT(mLinesInBuffer < mWindowCapacity, + "Need more rows than capacity!"); + mXFilter.ConvolveHorizontally(mRowBuffer.get(), mWindow[mLinesInBuffer++], + mHasAlpha); + } + + MOZ_ASSERT(mCurrentOutLine < mTargetSize.height, + "Writing past end of output"); + + while (mLinesInBuffer >= filterLength) { + DownscaleInputLine(); + + if (mCurrentOutLine == mTargetSize.height) { + break; // We're done. + } + + mYFilter.GetFilterOffsetAndLength(mCurrentOutLine, &filterOffset, + &filterLength); + } + } + + mCurrentInLine += 1; + + // If we're at the end of the part of the original image that has data, commit + // rows to shift us to the end. + if (mCurrentInLine == (mFrameRect.Y() + mFrameRect.Height())) { + SkipToRow(mOriginalSize.height - 1); + } +} + +bool Downscaler::HasInvalidation() const { + return mCurrentOutLine > mPrevInvalidatedLine; +} + +DownscalerInvalidRect Downscaler::TakeInvalidRect() { + if (MOZ_UNLIKELY(!HasInvalidation())) { + return DownscalerInvalidRect(); + } + + DownscalerInvalidRect invalidRect; + + // Compute the target size invalid rect. + if (mFlipVertically) { + // We need to flip it. This will implicitly flip the original size invalid + // rect, since we compute it by scaling this rect. + invalidRect.mTargetSizeRect = + IntRect(0, mTargetSize.height - mCurrentOutLine, mTargetSize.width, + mCurrentOutLine - mPrevInvalidatedLine); + } else { + invalidRect.mTargetSizeRect = + IntRect(0, mPrevInvalidatedLine, mTargetSize.width, + mCurrentOutLine - mPrevInvalidatedLine); + } + + mPrevInvalidatedLine = mCurrentOutLine; + + // Compute the original size invalid rect. + invalidRect.mOriginalSizeRect = invalidRect.mTargetSizeRect; + invalidRect.mOriginalSizeRect.ScaleRoundOut(mScale.xScale, mScale.yScale); + + return invalidRect; +} + +void Downscaler::DownscaleInputLine() { + MOZ_ASSERT(mOutputBuffer); + MOZ_ASSERT(mCurrentOutLine < mTargetSize.height, + "Writing past end of output"); + + int32_t filterOffset = 0; + int32_t filterLength = 0; + mYFilter.GetFilterOffsetAndLength(mCurrentOutLine, &filterOffset, + &filterLength); + + int32_t currentOutLine = mFlipVertically + ? mTargetSize.height - (mCurrentOutLine + 1) + : mCurrentOutLine; + MOZ_ASSERT(currentOutLine >= 0); + + uint8_t* outputLine = + &mOutputBuffer[currentOutLine * mTargetSize.width * sizeof(uint32_t)]; + mYFilter.ConvolveVertically(mWindow.get(), outputLine, mCurrentOutLine, + mXFilter.NumValues(), mHasAlpha); + + mCurrentOutLine += 1; + + if (mCurrentOutLine == mTargetSize.height) { + // We're done. + return; + } + + int32_t newFilterOffset = 0; + int32_t newFilterLength = 0; + mYFilter.GetFilterOffsetAndLength(mCurrentOutLine, &newFilterOffset, + &newFilterLength); + + int diff = newFilterOffset - filterOffset; + MOZ_ASSERT(diff >= 0, "Moving backwards in the filter?"); + + // Shift the buffer. We're just moving pointers here, so this is cheap. + mLinesInBuffer -= diff; + mLinesInBuffer = std::min(std::max(mLinesInBuffer, 0), mWindowCapacity); + + // If we already have enough rows to satisfy the filter, there is no need + // to swap as we won't be writing more before the next convolution. + if (filterLength > mLinesInBuffer) { + for (int32_t i = 0; i < mLinesInBuffer; ++i) { + swap(mWindow[i], mWindow[filterLength - mLinesInBuffer + i]); + } + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/Downscaler.h b/image/Downscaler.h new file mode 100644 index 0000000000..e5fcf0387e --- /dev/null +++ b/image/Downscaler.h @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Downscaler is a high-quality, streaming image downscaler based upon Skia's + * scaling implementation. + */ + +#ifndef mozilla_image_Downscaler_h +#define mozilla_image_Downscaler_h + +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "gfxPoint.h" +#include "nsRect.h" +#include "mozilla/gfx/ConvolutionFilter.h" +#include "mozilla/gfx/Matrix.h" + +namespace mozilla { +namespace image { + +/** + * DownscalerInvalidRect wraps two invalidation rects: one in terms of the + * original image size, and one in terms of the target size. + */ +struct DownscalerInvalidRect { + nsIntRect mOriginalSizeRect; + nsIntRect mTargetSizeRect; +}; + +/** + * Downscaler is a high-quality, streaming image downscaler based upon Skia's + * scaling implementation. + * + * Decoders can construct a Downscaler once they know their target size, then + * call BeginFrame() for each frame they decode. They should write a decoded row + * into the buffer returned by RowBuffer(), and then call CommitRow() to signal + * that they have finished. + * + + * Because invalidations need to be computed in terms of the scaled version of + * the image, Downscaler also tracks them. Decoders can call HasInvalidation() + * and TakeInvalidRect() instead of tracking invalidations themselves. + */ +class Downscaler { + public: + /// Constructs a new Downscaler which to scale to size @aTargetSize. + explicit Downscaler(const nsIntSize& aTargetSize); + ~Downscaler(); + + const nsIntSize& OriginalSize() const { return mOriginalSize; } + const nsIntSize& TargetSize() const { return mTargetSize; } + const nsIntSize FrameSize() const { + return nsIntSize(mFrameRect.Width(), mFrameRect.Height()); + } + const gfx::MatrixScalesDouble& Scale() const { return mScale; } + + /** + * Begins a new frame and reinitializes the Downscaler. + * + * @param aOriginalSize The original size of this frame, before scaling. + * @param aFrameRect The region of the original image which has data. + * Every pixel outside @aFrameRect is considered blank and + * has zero alpha. + * @param aOutputBuffer The buffer to which the Downscaler should write its + * output; this is the same buffer where the Decoder + * would write its output when not downscaling during + * decode. + * @param aHasAlpha Whether or not this frame has an alpha channel. + * Performance is a little better if it doesn't have one. + * @param aFlipVertically If true, output rows will be written to the output + * buffer in reverse order vertically, which matches + * the way they are stored in some image formats. + */ + nsresult BeginFrame(const nsIntSize& aOriginalSize, + const Maybe& aFrameRect, + uint8_t* aOutputBuffer, bool aHasAlpha, + bool aFlipVertically = false); + + bool IsFrameComplete() const { + return mCurrentInLine >= mOriginalSize.height; + } + + /// Retrieves the buffer into which the Decoder should write each row. + uint8_t* RowBuffer() { + return mRowBuffer.get() + mFrameRect.X() * sizeof(uint32_t); + } + + /// Clears the current row buffer. + void ClearRow() { ClearRestOfRow(0); } + + /// Clears the current row buffer starting at @aStartingAtCol. + void ClearRestOfRow(uint32_t aStartingAtCol); + + /// Signals that the decoder has finished writing a row into the row buffer. + void CommitRow(); + + /// Returns true if there is a non-empty invalid rect available. + bool HasInvalidation() const; + + /// Takes the Downscaler's current invalid rect and resets it. + DownscalerInvalidRect TakeInvalidRect(); + + /** + * Resets the Downscaler's position in the image, for a new progressive pass + * over the same frame. Because the same data structures can be reused, this + * is more efficient than calling BeginFrame. + */ + void ResetForNextProgressivePass(); + + private: + void DownscaleInputLine(); + void ReleaseWindow(); + void SkipToRow(int32_t aRow); + + nsIntSize mOriginalSize; + nsIntSize mTargetSize; + nsIntRect mFrameRect; + gfx::MatrixScalesDouble mScale; + + uint8_t* mOutputBuffer; + + UniquePtr mRowBuffer; + UniquePtr mWindow; + + gfx::ConvolutionFilter mXFilter; + gfx::ConvolutionFilter mYFilter; + + int32_t mWindowCapacity; + + int32_t mLinesInBuffer; + int32_t mPrevInvalidatedLine; + int32_t mCurrentOutLine; + int32_t mCurrentInLine; + + bool mHasAlpha : 1; + bool mFlipVertically : 1; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_Downscaler_h diff --git a/image/DownscalingFilter.h b/image/DownscalingFilter.h new file mode 100644 index 0000000000..1233e7be0b --- /dev/null +++ b/image/DownscalingFilter.h @@ -0,0 +1,312 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * DownscalingSurfaceFilter is a SurfaceFilter implementation for use with + * SurfacePipe which performs Lanczos downscaling. + * + * It's in this header file, separated from the other SurfaceFilters, because + * some preprocessor magic is necessary to ensure that there aren't compilation + * issues on platforms where Skia is unavailable. + */ + +#ifndef mozilla_image_DownscalingFilter_h +#define mozilla_image_DownscalingFilter_h + +#include +#include +#include + +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/gfx/2D.h" + +#include "mozilla/gfx/ConvolutionFilter.h" + +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////////////// +// DownscalingFilter +////////////////////////////////////////////////////////////////////////////// + +template +class DownscalingFilter; + +/** + * A configuration struct for DownscalingConfig. + */ +struct DownscalingConfig { + template + using Filter = DownscalingFilter; + gfx::IntSize mInputSize; /// The size of the input image. We'll downscale + /// from this size to the input size of the next + /// SurfaceFilter in the chain. + gfx::SurfaceFormat mFormat; /// The pixel format - BGRA or BGRX. (BGRX has + /// slightly better performance.) +}; + +/** + * DownscalingFilter performs Lanczos downscaling, taking image input data at + * one size and outputting it rescaled to a different size. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class DownscalingFilter final : public SurfaceFilter { + public: + DownscalingFilter() + : mWindowCapacity(0), + mRowsInWindow(0), + mInputRow(0), + mOutputRow(0), + mHasAlpha(true) {} + + ~DownscalingFilter() { ReleaseWindow(); } + + template + nsresult Configure(const DownscalingConfig& aConfig, const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + if (mNext.InputSize() == aConfig.mInputSize) { + NS_WARNING("Created a downscaler, but not downscaling?"); + return NS_ERROR_INVALID_ARG; + } + if (mNext.InputSize().width > aConfig.mInputSize.width) { + NS_WARNING("Created a downscaler, but width is larger"); + return NS_ERROR_INVALID_ARG; + } + if (mNext.InputSize().height > aConfig.mInputSize.height) { + NS_WARNING("Created a downscaler, but height is larger"); + return NS_ERROR_INVALID_ARG; + } + if (aConfig.mInputSize.width <= 0 || aConfig.mInputSize.height <= 0) { + NS_WARNING("Invalid input size for DownscalingFilter"); + return NS_ERROR_INVALID_ARG; + } + + mInputSize = aConfig.mInputSize; + gfx::IntSize outputSize = mNext.InputSize(); + mScale = + gfx::MatrixScalesDouble(double(mInputSize.width) / outputSize.width, + double(mInputSize.height) / outputSize.height); + mHasAlpha = aConfig.mFormat == gfx::SurfaceFormat::OS_RGBA; + + ReleaseWindow(); + + auto resizeMethod = gfx::ConvolutionFilter::ResizeMethod::LANCZOS3; + if (!mXFilter.ComputeResizeFilter(resizeMethod, mInputSize.width, + outputSize.width) || + !mYFilter.ComputeResizeFilter(resizeMethod, mInputSize.height, + outputSize.height)) { + NS_WARNING("Failed to compute filters for image downscaling"); + return NS_ERROR_OUT_OF_MEMORY; + } + + // Allocate the buffer, which contains scanlines of the input image. + mRowBuffer.reset(new (fallible) + uint8_t[PaddedWidthInBytes(mInputSize.width)]); + if (MOZ_UNLIKELY(!mRowBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Clear the buffer to avoid writing uninitialized memory to the output. + memset(mRowBuffer.get(), 0, PaddedWidthInBytes(mInputSize.width)); + + // Allocate the window, which contains horizontally downscaled scanlines. + // (We can store scanlines which are already downscaled because our + // downscaling filter is separable.) + mWindowCapacity = mYFilter.MaxFilter(); + mWindow.reset(new (fallible) uint8_t*[mWindowCapacity]); + if (MOZ_UNLIKELY(!mWindow)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Allocate the "window" of recent rows that we keep in memory as input for + // the downscaling code. We intentionally iterate through the entire array + // even if an allocation fails, to ensure that all the pointers in it are + // either valid or nullptr. That in turn ensures that ReleaseWindow() can + // clean up correctly. + bool anyAllocationFailed = false; + const size_t windowRowSizeInBytes = PaddedWidthInBytes(outputSize.width); + for (int32_t i = 0; i < mWindowCapacity; ++i) { + mWindow[i] = new (fallible) uint8_t[windowRowSizeInBytes]; + anyAllocationFailed = anyAllocationFailed || mWindow[i] == nullptr; + } + + if (MOZ_UNLIKELY(anyAllocationFailed)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + ConfigureFilter(mInputSize, sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + Maybe invalidRect = mNext.TakeInvalidRect(); + + if (invalidRect) { + // Compute the input space invalid rect by scaling. + invalidRect->mInputSpaceRect.ScaleRoundOut(mScale.xScale, mScale.yScale); + } + + return invalidRect; + } + + protected: + uint8_t* DoResetToFirstRow() override { + mNext.ResetToFirstRow(); + + mInputRow = 0; + mOutputRow = 0; + mRowsInWindow = 0; + + return GetRowPointer(); + } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + if (mInputRow >= mInputSize.height) { + NS_WARNING("Advancing DownscalingFilter past the end of the input"); + return nullptr; + } + + if (mOutputRow >= mNext.InputSize().height) { + NS_WARNING("Advancing DownscalingFilter past the end of the output"); + return nullptr; + } + + int32_t filterOffset = 0; + int32_t filterLength = 0; + mYFilter.GetFilterOffsetAndLength(mOutputRow, &filterOffset, &filterLength); + + int32_t inputRowToRead = filterOffset + mRowsInWindow; + MOZ_ASSERT(mInputRow <= inputRowToRead, "Reading past end of input"); + if (mInputRow == inputRowToRead) { + MOZ_RELEASE_ASSERT(mRowsInWindow < mWindowCapacity, + "Need more rows than capacity!"); + mXFilter.ConvolveHorizontally(aInputRow, mWindow[mRowsInWindow++], + mHasAlpha); + } + + MOZ_ASSERT(mOutputRow < mNext.InputSize().height, + "Writing past end of output"); + + while (mRowsInWindow >= filterLength) { + DownscaleInputRow(); + + if (mOutputRow == mNext.InputSize().height) { + break; // We're done. + } + + mYFilter.GetFilterOffsetAndLength(mOutputRow, &filterOffset, + &filterLength); + } + + mInputRow++; + + return mInputRow < mInputSize.height ? GetRowPointer() : nullptr; + } + + uint8_t* DoAdvanceRow() override { + return DoAdvanceRowFromBuffer(mRowBuffer.get()); + } + + private: + uint8_t* GetRowPointer() const { return mRowBuffer.get(); } + + static size_t PaddedWidthInBytes(size_t aLogicalWidth) { + // Convert from width in BGRA/BGRX pixels to width in bytes, padding + // to handle overreads by the SIMD code inside Skia. + return gfx::ConvolutionFilter::PadBytesForSIMD(aLogicalWidth * + sizeof(uint32_t)); + } + + void DownscaleInputRow() { + MOZ_ASSERT(mOutputRow < mNext.InputSize().height, + "Writing past end of output"); + + int32_t filterOffset = 0; + int32_t filterLength = 0; + mYFilter.GetFilterOffsetAndLength(mOutputRow, &filterOffset, &filterLength); + + mNext.template WriteUnsafeComputedRow([&](uint32_t* aRow, + uint32_t aLength) { + mYFilter.ConvolveVertically(mWindow.get(), + reinterpret_cast(aRow), mOutputRow, + mXFilter.NumValues(), mHasAlpha); + }); + + mOutputRow++; + + if (mOutputRow == mNext.InputSize().height) { + return; // We're done. + } + + int32_t newFilterOffset = 0; + int32_t newFilterLength = 0; + mYFilter.GetFilterOffsetAndLength(mOutputRow, &newFilterOffset, + &newFilterLength); + + int diff = newFilterOffset - filterOffset; + MOZ_ASSERT(diff >= 0, "Moving backwards in the filter?"); + + // Shift the buffer. We're just moving pointers here, so this is cheap. + mRowsInWindow -= diff; + mRowsInWindow = std::min(std::max(mRowsInWindow, 0), mWindowCapacity); + + // If we already have enough rows to satisfy the filter, there is no need + // to swap as we won't be writing more before the next convolution. + if (filterLength > mRowsInWindow) { + for (int32_t i = 0; i < mRowsInWindow; ++i) { + std::swap(mWindow[i], mWindow[filterLength - mRowsInWindow + i]); + } + } + } + + void ReleaseWindow() { + if (!mWindow) { + return; + } + + for (int32_t i = 0; i < mWindowCapacity; ++i) { + delete[] mWindow[i]; + } + + mWindow = nullptr; + mWindowCapacity = 0; + } + + Next mNext; /// The next SurfaceFilter in the chain. + + gfx::IntSize mInputSize; /// The size of the input image. + gfx::MatrixScalesDouble mScale; /// The scale factors in each dimension. + /// Computed from @mInputSize and + /// the next filter's input size. + + UniquePtr mRowBuffer; /// The buffer into which input is written. + UniquePtr mWindow; /// The last few rows which were written. + + gfx::ConvolutionFilter mXFilter; /// The Lanczos filter in X. + gfx::ConvolutionFilter mYFilter; /// The Lanczos filter in Y. + + int32_t mWindowCapacity; /// How many rows the window contains. + + int32_t mRowsInWindow; /// How many rows we've buffered in the window. + int32_t mInputRow; /// The current row we're reading. (0-indexed) + int32_t mOutputRow; /// The current row we're writing. (0-indexed) + + bool mHasAlpha; /// If true, the image has transparency. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_DownscalingFilter_h diff --git a/image/DynamicImage.cpp b/image/DynamicImage.cpp new file mode 100644 index 0000000000..923e91d352 --- /dev/null +++ b/image/DynamicImage.cpp @@ -0,0 +1,298 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DynamicImage.h" +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGImageContext.h" +#include "ImageRegion.h" +#include "Orientation.h" +#include "mozilla/image/Resolution.h" + +#include "mozilla/MemoryReporting.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +namespace mozilla::image { + +// Inherited methods from Image. + +already_AddRefed DynamicImage::GetProgressTracker() { + return nullptr; +} + +size_t DynamicImage::SizeOfSourceWithComputedFallback( + SizeOfState& aState) const { + return 0; +} + +void DynamicImage::CollectSizeOfSurfaces( + nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const { + // We can't report anything useful because gfxDrawable doesn't expose this + // information. +} + +void DynamicImage::IncrementAnimationConsumers() {} + +void DynamicImage::DecrementAnimationConsumers() {} + +#ifdef DEBUG +uint32_t DynamicImage::GetAnimationConsumers() { return 0; } +#endif + +nsresult DynamicImage::OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) { + return NS_OK; +} + +nsresult DynamicImage::OnImageDataComplete(nsIRequest* aRequest, + nsresult aStatus, bool aLastPart) { + return NS_OK; +} + +void DynamicImage::OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) {} + +void DynamicImage::SetInnerWindowID(uint64_t aInnerWindowId) {} + +uint64_t DynamicImage::InnerWindowID() const { return 0; } + +bool DynamicImage::HasError() { return !mDrawable; } + +void DynamicImage::SetHasError() {} + +nsIURI* DynamicImage::GetURI() const { return nullptr; } + +// Methods inherited from XPCOM interfaces. + +NS_IMPL_ISUPPORTS(DynamicImage, imgIContainer) + +NS_IMETHODIMP +DynamicImage::GetWidth(int32_t* aWidth) { + *aWidth = mDrawable->Size().width; + return NS_OK; +} + +NS_IMETHODIMP +DynamicImage::GetHeight(int32_t* aHeight) { + *aHeight = mDrawable->Size().height; + return NS_OK; +} + +void DynamicImage::MediaFeatureValuesChangedAllDocuments( + const mozilla::MediaFeatureChange& aChange) {} + +nsresult DynamicImage::GetNativeSizes(nsTArray&) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +size_t DynamicImage::GetNativeSizesLength() { return 0; } + +NS_IMETHODIMP +DynamicImage::GetIntrinsicSize(nsSize* aSize) { + IntSize intSize(mDrawable->Size()); + *aSize = nsSize(intSize.width, intSize.height); + return NS_OK; +} + +Maybe DynamicImage::GetIntrinsicRatio() { + auto size = mDrawable->Size(); + return Some(AspectRatio::FromSize(size.width, size.height)); +} + +NS_IMETHODIMP_(Orientation) +DynamicImage::GetOrientation() { return Orientation(); } + +NS_IMETHODIMP_(Resolution) +DynamicImage::GetResolution() { return {}; } + +NS_IMETHODIMP +DynamicImage::GetType(uint16_t* aType) { + *aType = imgIContainer::TYPE_RASTER; + return NS_OK; +} + +NS_IMETHODIMP +DynamicImage::GetProviderId(uint32_t* aId) { + *aId = 0; + return NS_OK; +} + +NS_IMETHODIMP +DynamicImage::GetAnimated(bool* aAnimated) { + *aAnimated = false; + return NS_OK; +} + +NS_IMETHODIMP_(already_AddRefed) +DynamicImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + IntSize size(mDrawable->Size()); + return GetFrameAtSize(IntSize(size.width, size.height), aWhichFrame, aFlags); +} + +NS_IMETHODIMP_(already_AddRefed) +DynamicImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + RefPtr dt = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + aSize, SurfaceFormat::OS_RGBA); + if (!dt || !dt->IsValid()) { + gfxWarning() + << "DynamicImage::GetFrame failed in CreateOffscreenContentDrawTarget"; + return nullptr; + } + gfxContext context(dt); + + auto result = Draw(&context, aSize, ImageRegion::Create(aSize), aWhichFrame, + SamplingFilter::POINT, SVGImageContext(), aFlags, 1.0); + + return result == ImgDrawResult::SUCCESS ? dt->Snapshot() : nullptr; +} + +NS_IMETHODIMP_(bool) +DynamicImage::WillDrawOpaqueNow() { return false; } + +NS_IMETHODIMP_(bool) +DynamicImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + return false; +} + +NS_IMETHODIMP_(ImgDrawResult) +DynamicImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + return ImgDrawResult::NOT_SUPPORTED; +} + +NS_IMETHODIMP_(ImgDrawResult) +DynamicImage::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + MOZ_ASSERT(!aSize.IsEmpty(), "Unexpected empty size"); + + IntSize drawableSize(mDrawable->Size()); + + if (aSize == drawableSize) { + gfxUtils::DrawPixelSnapped(aContext, mDrawable, SizeDouble(drawableSize), + aRegion, SurfaceFormat::OS_RGBA, aSamplingFilter, + aOpacity); + return ImgDrawResult::SUCCESS; + } + + MatrixScalesDouble scale(double(aSize.width) / drawableSize.width, + double(aSize.height) / drawableSize.height); + + ImageRegion region(aRegion); + region.Scale(1.0 / scale.xScale, 1.0 / scale.yScale); + + gfxContextMatrixAutoSaveRestore saveMatrix(aContext); + aContext->Multiply(gfxMatrix::Scaling(scale)); + + gfxUtils::DrawPixelSnapped(aContext, mDrawable, SizeDouble(drawableSize), + region, SurfaceFormat::OS_RGBA, aSamplingFilter, + aOpacity); + return ImgDrawResult::SUCCESS; +} + +NS_IMETHODIMP +DynamicImage::StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) { + return NS_OK; +} + +bool DynamicImage::StartDecodingWithResult(uint32_t aFlags, + uint32_t aWhichFrame) { + return true; +} + +bool DynamicImage::HasDecodedPixels() { return true; } + +imgIContainer::DecodeResult DynamicImage::RequestDecodeWithResult( + uint32_t aFlags, uint32_t aWhichFrame) { + return imgIContainer::DECODE_SURFACE_AVAILABLE; +} + +NS_IMETHODIMP +DynamicImage::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) { + return NS_OK; +} + +NS_IMETHODIMP +DynamicImage::LockImage() { return NS_OK; } + +NS_IMETHODIMP +DynamicImage::UnlockImage() { return NS_OK; } + +NS_IMETHODIMP +DynamicImage::RequestDiscard() { return NS_OK; } + +NS_IMETHODIMP_(void) +DynamicImage::RequestRefresh(const mozilla::TimeStamp& aTime) {} + +NS_IMETHODIMP +DynamicImage::GetAnimationMode(uint16_t* aAnimationMode) { + *aAnimationMode = kNormalAnimMode; + return NS_OK; +} + +NS_IMETHODIMP +DynamicImage::SetAnimationMode(uint16_t aAnimationMode) { return NS_OK; } + +NS_IMETHODIMP +DynamicImage::ResetAnimation() { return NS_OK; } + +NS_IMETHODIMP_(float) +DynamicImage::GetFrameIndex(uint32_t aWhichFrame) { return 0; } + +NS_IMETHODIMP_(int32_t) +DynamicImage::GetFirstFrameDelay() { return 0; } + +NS_IMETHODIMP_(void) +DynamicImage::SetAnimationStartTime(const mozilla::TimeStamp& aTime) {} + +nsIntSize DynamicImage::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + IntSize size(mDrawable->Size()); + return nsIntSize(size.width, size.height); +} + +NS_IMETHODIMP_(nsIntRect) +DynamicImage::GetImageSpaceInvalidationRect(const nsIntRect& aRect) { + return aRect; +} + +already_AddRefed DynamicImage::Unwrap() { + nsCOMPtr self(this); + return self.forget(); +} + +void DynamicImage::PropagateUseCounters(dom::Document*) { + // No use counters. +} + +nsresult DynamicImage::GetHotspotX(int32_t* aX) { + return Image::GetHotspotX(aX); +} + +nsresult DynamicImage::GetHotspotY(int32_t* aY) { + return Image::GetHotspotY(aY); +} + +} // namespace mozilla::image diff --git a/image/DynamicImage.h b/image/DynamicImage.h new file mode 100644 index 0000000000..2b8a833958 --- /dev/null +++ b/image/DynamicImage.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_DynamicImage_h +#define mozilla_image_DynamicImage_h + +#include "mozilla/MemoryReporting.h" +#include "gfxDrawable.h" +#include "Image.h" + +namespace mozilla { +namespace image { + +/** + * An Image that is dynamically created. The content of the image is provided by + * a gfxDrawable. It's anticipated that most uses of DynamicImage will be + * ephemeral. + */ +class DynamicImage : public Image { + public: + NS_DECL_ISUPPORTS + NS_DECL_IMGICONTAINER + + explicit DynamicImage(gfxDrawable* aDrawable) : mDrawable(aDrawable) { + MOZ_ASSERT(aDrawable, "Must have a gfxDrawable to wrap"); + } + + // Inherited methods from Image. + virtual already_AddRefed GetProgressTracker() override; + virtual size_t SizeOfSourceWithComputedFallback( + SizeOfState& aState) const override; + virtual void CollectSizeOfSurfaces(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const override; + + virtual void IncrementAnimationConsumers() override; + virtual void DecrementAnimationConsumers() override; +#ifdef DEBUG + virtual uint32_t GetAnimationConsumers() override; +#endif + + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) override; + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aStatus, + bool aLastPart) override; + + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override; + + virtual void SetInnerWindowID(uint64_t aInnerWindowId) override; + virtual uint64_t InnerWindowID() const override; + + virtual bool HasError() override; + virtual void SetHasError() override; + + nsIURI* GetURI() const override; + + private: + virtual ~DynamicImage() {} + + RefPtr mDrawable; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_DynamicImage_h diff --git a/image/FrameAnimator.cpp b/image/FrameAnimator.cpp new file mode 100644 index 0000000000..f21877d81a --- /dev/null +++ b/image/FrameAnimator.cpp @@ -0,0 +1,519 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FrameAnimator.h" + +#include + +#include "LookupResult.h" +#include "RasterImage.h" +#include "imgIContainer.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_image.h" + +namespace mozilla { + +using namespace gfx; + +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// AnimationState implementation. +/////////////////////////////////////////////////////////////////////////////// + +const gfx::IntRect AnimationState::UpdateState( + RasterImage* aImage, const gfx::IntSize& aSize, + bool aAllowInvalidation /* = true */) { + LookupResult result = SurfaceCache::Lookup( + ImageKey(aImage), + RasterSurfaceKey(aSize, DefaultSurfaceFlags(), PlaybackType::eAnimated), + /* aMarkUsed = */ false); + + return UpdateStateInternal(result, aSize, aAllowInvalidation); +} + +const gfx::IntRect AnimationState::UpdateStateInternal( + LookupResult& aResult, const gfx::IntSize& aSize, + bool aAllowInvalidation /* = true */) { + // Update mDiscarded and mIsCurrentlyDecoded. + if (aResult.Type() == MatchType::NOT_FOUND) { + // no frames, we've either been discarded, or never been decoded before. + mDiscarded = mHasBeenDecoded; + mIsCurrentlyDecoded = false; + } else if (aResult.Type() == MatchType::PENDING) { + // no frames yet, but a decoder is or will be working on it. + mDiscarded = false; + mIsCurrentlyDecoded = false; + mHasRequestedDecode = true; + } else { + MOZ_ASSERT(aResult.Type() == MatchType::EXACT); + mDiscarded = false; + mHasRequestedDecode = true; + + // If we can seek to the current animation frame we consider it decoded. + // Animated images are never fully decoded unless very short. + // Note that we use GetFrame instead of Seek here. The difference is that + // Seek eventually calls AnimationFrameBuffer::Get with aForDisplay == true, + // whereas GetFrame calls AnimationFrameBuffer::Get with aForDisplay == + // false. The aForDisplay can change whether those functions succeed or not + // (only for the first frame). Since this is not for display we want to pass + // aForDisplay == false, but also for consistency with + // RequestRefresh/AdvanceFrame, because we want our state to be in sync with + // those functions. The user of Seek (GetCompositedFrame) doesn't need to be + // in sync with our state. + RefPtr currentFrame = + bool(aResult.Surface()) + ? aResult.Surface().GetFrame(mCurrentAnimationFrameIndex) + : nullptr; + mIsCurrentlyDecoded = !!currentFrame; + } + + gfx::IntRect ret; + + if (aAllowInvalidation) { + // Update the value of mCompositedFrameInvalid. + if (mIsCurrentlyDecoded) { + // It is safe to clear mCompositedFrameInvalid safe to do for images that + // are fully decoded but aren't finished animating because before we paint + // the refresh driver will call into us to advance to the correct frame, + // and that will succeed because we have all the frames. + if (mCompositedFrameInvalid) { + // Invalidate if we are marking the composited frame valid. + ret.SizeTo(aSize); + } + mCompositedFrameInvalid = false; + } else { + if (mHasRequestedDecode) { + MOZ_ASSERT(StaticPrefs::image_mem_animated_discardable_AtStartup()); + mCompositedFrameInvalid = true; + } + } + // Otherwise don't change the value of mCompositedFrameInvalid, it will be + // updated by RequestRefresh. + } + + return ret; +} + +void AnimationState::NotifyDecodeComplete() { mHasBeenDecoded = true; } + +void AnimationState::ResetAnimation() { mCurrentAnimationFrameIndex = 0; } + +void AnimationState::SetAnimationMode(uint16_t aAnimationMode) { + mAnimationMode = aAnimationMode; +} + +void AnimationState::UpdateKnownFrameCount(uint32_t aFrameCount) { + if (aFrameCount <= mFrameCount) { + // Nothing to do. Since we can redecode animated images, we may see the same + // sequence of updates replayed again, so seeing a smaller frame count than + // what we already know about doesn't indicate an error. + return; + } + + MOZ_ASSERT(!mHasBeenDecoded, "Adding new frames after decoding is finished?"); + MOZ_ASSERT(aFrameCount <= mFrameCount + 1, "Skipped a frame?"); + + mFrameCount = aFrameCount; +} + +Maybe AnimationState::FrameCount() const { + return mHasBeenDecoded ? Some(mFrameCount) : Nothing(); +} + +void AnimationState::SetFirstFrameRefreshArea(const IntRect& aRefreshArea) { + mFirstFrameRefreshArea = aRefreshArea; +} + +void AnimationState::InitAnimationFrameTimeIfNecessary() { + if (mCurrentAnimationFrameTime.IsNull()) { + mCurrentAnimationFrameTime = TimeStamp::Now(); + } +} + +void AnimationState::SetAnimationFrameTime(const TimeStamp& aTime) { + mCurrentAnimationFrameTime = aTime; +} + +bool AnimationState::MaybeAdvanceAnimationFrameTime(const TimeStamp& aTime) { + if (!StaticPrefs::image_animated_resume_from_last_displayed() || + mCurrentAnimationFrameTime >= aTime) { + return false; + } + + // We are configured to stop an animation when it is out of view, and restart + // it from the same point when it comes back into view. The same applies if it + // was discarded while out of view. + mCurrentAnimationFrameTime = aTime; + return true; +} + +uint32_t AnimationState::GetCurrentAnimationFrameIndex() const { + return mCurrentAnimationFrameIndex; +} + +FrameTimeout AnimationState::LoopLength() const { + // If we don't know the loop length yet, we have to treat it as infinite. + if (!mLoopLength) { + return FrameTimeout::Forever(); + } + + MOZ_ASSERT(mHasBeenDecoded, + "We know the loop length but decoding isn't done?"); + + // If we're not looping, a single loop time has no meaning. + if (mAnimationMode != imgIContainer::kNormalAnimMode) { + return FrameTimeout::Forever(); + } + + return *mLoopLength; +} + +/////////////////////////////////////////////////////////////////////////////// +// FrameAnimator implementation. +/////////////////////////////////////////////////////////////////////////////// + +TimeStamp FrameAnimator::GetCurrentImgFrameEndTime( + AnimationState& aState, FrameTimeout aCurrentTimeout) const { + if (aCurrentTimeout == FrameTimeout::Forever()) { + // We need to return a sentinel value in this case, because our logic + // doesn't work correctly if we have an infinitely long timeout. We use one + // year in the future as the sentinel because it works with the loop in + // RequestRefresh() below. + // XXX(seth): It'd be preferable to make our logic work correctly with + // infinitely long timeouts. + return TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(31536000.0); + } + + TimeDuration durationOfTimeout = + TimeDuration::FromMilliseconds(double(aCurrentTimeout.AsMilliseconds())); + return aState.mCurrentAnimationFrameTime + durationOfTimeout; +} + +RefreshResult FrameAnimator::AdvanceFrame(AnimationState& aState, + DrawableSurface& aFrames, + RefPtr& aCurrentFrame, + TimeStamp aTime) { + AUTO_PROFILER_LABEL("FrameAnimator::AdvanceFrame", GRAPHICS); + + RefreshResult ret; + + // Determine what the next frame is, taking into account looping. + uint32_t currentFrameIndex = aState.mCurrentAnimationFrameIndex; + uint32_t nextFrameIndex = currentFrameIndex + 1; + + // Check if we're at the end of the loop. (FrameCount() returns Nothing() if + // we don't know the total count yet.) + if (aState.FrameCount() == Some(nextFrameIndex)) { + // If we are not looping forever, initialize the loop counter + if (aState.mLoopRemainingCount < 0 && aState.LoopCount() >= 0) { + aState.mLoopRemainingCount = aState.LoopCount(); + } + + // If animation mode is "loop once", or we're at end of loop counter, + // it's time to stop animating. + if (aState.mAnimationMode == imgIContainer::kLoopOnceAnimMode || + aState.mLoopRemainingCount == 0) { + ret.mAnimationFinished = true; + } + + nextFrameIndex = 0; + + if (aState.mLoopRemainingCount > 0) { + aState.mLoopRemainingCount--; + } + + // If we're done, exit early. + if (ret.mAnimationFinished) { + return ret; + } + } + + if (nextFrameIndex >= aState.KnownFrameCount()) { + // We've already advanced to the last decoded frame, nothing more we can do. + // We're blocked by network/decoding from displaying the animation at the + // rate specified, so that means the frame we are displaying (the latest + // available) is the frame we want to be displaying at this time. So we + // update the current animation time. If we didn't update the current + // animation time then it could lag behind, which would indicate that we are + // behind in the animation and should try to catch up. When we are done + // decoding (and thus can loop around back to the start of the animation) we + // would then jump to a random point in the animation to try to catch up. + // But we were never behind in the animation. + aState.mCurrentAnimationFrameTime = aTime; + return ret; + } + + // There can be frames in the surface cache with index >= KnownFrameCount() + // which GetRawFrame() can access because an async decoder has decoded them, + // but which AnimationState doesn't know about yet because we haven't received + // the appropriate notification on the main thread. Make sure we stay in sync + // with AnimationState. + MOZ_ASSERT(nextFrameIndex < aState.KnownFrameCount()); + RefPtr nextFrame = aFrames.GetFrame(nextFrameIndex); + + // We should always check to see if we have the next frame even if we have + // previously finished decoding. If we needed to redecode (e.g. due to a draw + // failure) we would have discarded all the old frames and may not yet have + // the new ones. DrawableSurface::RawAccessRef promises to only return + // finished frames. + if (!nextFrame) { + // Uh oh, the frame we want to show is currently being decoded (partial). + // Similar to the above case, we could be blocked by network or decoding, + // and so we should advance our current time rather than risk jumping + // through the animation. We will wait until the next refresh driver tick + // and try again. + aState.mCurrentAnimationFrameTime = aTime; + return ret; + } + + if (nextFrame->GetTimeout() == FrameTimeout::Forever()) { + ret.mAnimationFinished = true; + } + + if (nextFrameIndex == 0) { + ret.mDirtyRect = aState.FirstFrameRefreshArea(); + } else { + ret.mDirtyRect = nextFrame->GetDirtyRect(); + } + + aState.mCurrentAnimationFrameTime = + GetCurrentImgFrameEndTime(aState, aCurrentFrame->GetTimeout()); + + // If we can get closer to the current time by a multiple of the image's loop + // time, we should. We can only do this if we're done decoding; otherwise, we + // don't know the full loop length, and LoopLength() will have to return + // FrameTimeout::Forever(). We also skip this for images with a finite loop + // count if we have initialized mLoopRemainingCount (it only gets initialized + // after one full loop). + FrameTimeout loopTime = aState.LoopLength(); + if (loopTime != FrameTimeout::Forever() && + (aState.LoopCount() < 0 || aState.mLoopRemainingCount >= 0)) { + TimeDuration delay = aTime - aState.mCurrentAnimationFrameTime; + if (delay.ToMilliseconds() > loopTime.AsMilliseconds()) { + // Explicitly use integer division to get the floor of the number of + // loops. + uint64_t loops = static_cast(delay.ToMilliseconds()) / + loopTime.AsMilliseconds(); + + // If we have a finite loop count limit the number of loops we advance. + if (aState.mLoopRemainingCount >= 0) { + MOZ_ASSERT(aState.LoopCount() >= 0); + loops = + std::min(loops, CheckedUint64(aState.mLoopRemainingCount).value()); + } + + aState.mCurrentAnimationFrameTime += + TimeDuration::FromMilliseconds(loops * loopTime.AsMilliseconds()); + + if (aState.mLoopRemainingCount >= 0) { + MOZ_ASSERT(loops <= CheckedUint64(aState.mLoopRemainingCount).value()); + aState.mLoopRemainingCount -= CheckedInt32(loops).value(); + } + } + } + + // Set currentAnimationFrameIndex at the last possible moment + aState.mCurrentAnimationFrameIndex = nextFrameIndex; + aCurrentFrame = std::move(nextFrame); + aFrames.Advance(nextFrameIndex); + + // If we're here, we successfully advanced the frame. + ret.mFrameAdvanced = true; + + return ret; +} + +void FrameAnimator::ResetAnimation(AnimationState& aState) { + aState.ResetAnimation(); + + // Our surface provider is synchronized to our state, so we need to reset its + // state as well, if we still have one. + SurfaceCache::ResetAnimation( + ImageKey(mImage), + RasterSurfaceKey(mSize, DefaultSurfaceFlags(), PlaybackType::eAnimated)); + + // Calling Reset on the surface of the animation can cause discarding surface + // providers to throw out all their frames so refresh our state. + OrientedIntRect rect = + OrientedIntRect::FromUnknownRect(aState.UpdateState(mImage, mSize)); + + if (!rect.IsEmpty()) { + nsCOMPtr eventTarget = do_GetMainThread(); + RefPtr image = mImage; + nsCOMPtr ev = NS_NewRunnableFunction( + "FrameAnimator::ResetAnimation", + [=]() -> void { image->NotifyProgress(NoProgress, rect); }); + eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL); + } +} + +RefreshResult FrameAnimator::RequestRefresh(AnimationState& aState, + const TimeStamp& aTime) { + // By default, an empty RefreshResult. + RefreshResult ret; + + if (aState.IsDiscarded()) { + aState.MaybeAdvanceAnimationFrameTime(aTime); + return ret; + } + + // Get the animation frames once now, and pass them down to callees because + // the surface could be discarded at anytime on a different thread. This is + // must easier to reason about then trying to write code that is safe to + // having the surface disappear at anytime. + LookupResult result = SurfaceCache::Lookup( + ImageKey(mImage), + RasterSurfaceKey(mSize, DefaultSurfaceFlags(), PlaybackType::eAnimated), + /* aMarkUsed = */ true); + + ret.mDirtyRect = aState.UpdateStateInternal(result, mSize); + if (aState.IsDiscarded() || !result) { + aState.MaybeAdvanceAnimationFrameTime(aTime); + return ret; + } + + RefPtr currentFrame = + result.Surface().GetFrame(aState.mCurrentAnimationFrameIndex); + + // only advance the frame if the current time is greater than or + // equal to the current frame's end time. + if (!currentFrame) { + MOZ_ASSERT(StaticPrefs::image_mem_animated_discardable_AtStartup()); + MOZ_ASSERT(aState.GetHasRequestedDecode() && + !aState.GetIsCurrentlyDecoded()); + MOZ_ASSERT(aState.mCompositedFrameInvalid); + // Nothing we can do but wait for our previous current frame to be decoded + // again so we can determine what to do next. + aState.MaybeAdvanceAnimationFrameTime(aTime); + return ret; + } + + TimeStamp currentFrameEndTime = + GetCurrentImgFrameEndTime(aState, currentFrame->GetTimeout()); + + // If nothing has accessed the composited frame since the last time we + // advanced, then there is no point in continuing to advance the animation. + // This has the effect of freezing the animation while not in view. + if (!result.Surface().MayAdvance() && + aState.MaybeAdvanceAnimationFrameTime(aTime)) { + return ret; + } + + while (currentFrameEndTime <= aTime) { + TimeStamp oldFrameEndTime = currentFrameEndTime; + + RefreshResult frameRes = + AdvanceFrame(aState, result.Surface(), currentFrame, aTime); + + // Accumulate our result for returning to callers. + ret.Accumulate(frameRes); + + // currentFrame was updated by AdvanceFrame so it is still current. + currentFrameEndTime = + GetCurrentImgFrameEndTime(aState, currentFrame->GetTimeout()); + + // If we didn't advance a frame, and our frame end time didn't change, + // then we need to break out of this loop & wait for the frame(s) + // to finish downloading. + if (!frameRes.mFrameAdvanced && currentFrameEndTime == oldFrameEndTime) { + break; + } + } + + // We should only mark the composited frame as valid and reset the dirty rect + // if we advanced (meaning the next frame was actually produced somehow), the + // composited frame was previously invalid (so we may need to repaint + // everything) and either the frame index is valid (to know we were doing + // blending on the main thread, instead of on the decoder threads in advance), + // or the current frame is a full frame (blends off the main thread). + // + // If for some reason we forget to reset aState.mCompositedFrameInvalid, then + // GetCompositedFrame will fail, even if we have all the data available for + // display. + if (currentFrameEndTime > aTime && aState.mCompositedFrameInvalid) { + aState.mCompositedFrameInvalid = false; + ret.mDirtyRect = IntRect(IntPoint(0, 0), mSize); + } + + MOZ_ASSERT(!aState.mIsCurrentlyDecoded || !aState.mCompositedFrameInvalid); + + return ret; +} + +LookupResult FrameAnimator::GetCompositedFrame(AnimationState& aState, + bool aMarkUsed) { + LookupResult result = SurfaceCache::Lookup( + ImageKey(mImage), + RasterSurfaceKey(mSize, DefaultSurfaceFlags(), PlaybackType::eAnimated), + aMarkUsed); + + if (result) { + // If we are getting the frame directly (e.g. through tests or canvas), we + // need to ensure the animation is marked to allow advancing to the next + // frame. + result.Surface().MarkMayAdvance(); + } + + if (aState.mCompositedFrameInvalid) { + MOZ_ASSERT(StaticPrefs::image_mem_animated_discardable_AtStartup()); + MOZ_ASSERT(aState.GetHasRequestedDecode()); + MOZ_ASSERT(!aState.GetIsCurrentlyDecoded()); + + if (result.Type() == MatchType::EXACT) { + // If our composited frame is marked as invalid but our frames are in the + // surface cache we might just have not updated our internal state yet. + // This can happen if the image is not in a document so that + // RequestRefresh is not getting called to advance the frame. + // RequestRefresh would result in our composited frame getting marked as + // valid either at the end of RequestRefresh when we are able to advance + // to the current time or if advancing frames eventually causes us to + // decode all of the frames of the image resulting in DecodeComplete + // getting called which calls UpdateState. The reason we care about this + // is that img.decode promises won't resolve until GetCompositedFrame + // returns a frame. + OrientedIntRect rect = OrientedIntRect::FromUnknownRect( + aState.UpdateStateInternal(result, mSize)); + + if (!rect.IsEmpty()) { + nsCOMPtr eventTarget = do_GetMainThread(); + RefPtr image = mImage; + nsCOMPtr ev = NS_NewRunnableFunction( + "FrameAnimator::GetCompositedFrame", + [=]() -> void { image->NotifyProgress(NoProgress, rect); }); + eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL); + } + } + + // If it's still invalid we have to return. + if (aState.mCompositedFrameInvalid) { + if (result.Type() == MatchType::NOT_FOUND) { + return result; + } + return LookupResult(MatchType::PENDING); + } + } + + // Otherwise return the raw frame. DoBlend is required to ensure that we only + // hit this case if the frame is not paletted and doesn't require compositing. + if (!result) { + return result; + } + + // Seek to the appropriate frame. If seeking fails, it means that we couldn't + // get the frame we're looking for; treat this as if the lookup failed. + if (NS_FAILED(result.Surface().Seek(aState.mCurrentAnimationFrameIndex))) { + if (result.Type() == MatchType::NOT_FOUND) { + return result; + } + return LookupResult(MatchType::PENDING); + } + + return result; +} + +} // namespace image +} // namespace mozilla diff --git a/image/FrameAnimator.h b/image/FrameAnimator.h new file mode 100644 index 0000000000..40538a3beb --- /dev/null +++ b/image/FrameAnimator.h @@ -0,0 +1,343 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_FrameAnimator_h +#define mozilla_image_FrameAnimator_h + +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/TimeStamp.h" +#include "gfxTypes.h" +#include "imgFrame.h" +#include "nsCOMPtr.h" +#include "nsRect.h" +#include "SurfaceCache.h" + +namespace mozilla { +namespace image { + +class RasterImage; +class DrawableSurface; + +class AnimationState { + public: + explicit AnimationState(uint16_t aAnimationMode) + : mFrameCount(0), + mCurrentAnimationFrameIndex(0), + mLoopRemainingCount(-1), + mLoopCount(-1), + mFirstFrameTimeout(FrameTimeout::FromRawMilliseconds(0)), + mAnimationMode(aAnimationMode), + mHasBeenDecoded(false), + mHasRequestedDecode(false), + mIsCurrentlyDecoded(false), + mCompositedFrameInvalid(false), + mDiscarded(false) {} + + /** + * Call this whenever a decode completes, a decode starts, or the image is + * discarded. It will update the internal state. Specifically mDiscarded, + * mCompositedFrameInvalid, and mIsCurrentlyDecoded. If aAllowInvalidation + * is true then returns a rect to invalidate. + */ + const gfx::IntRect UpdateState(RasterImage* aImage, const gfx::IntSize& aSize, + bool aAllowInvalidation = true); + + private: + const gfx::IntRect UpdateStateInternal(LookupResult& aResult, + const gfx::IntSize& aSize, + bool aAllowInvalidation = true); + + public: + /** + * Call when a decode of this image has been completed. + */ + void NotifyDecodeComplete(); + + /** + * Returns true if this image has been fully decoded before. + */ + bool GetHasBeenDecoded() { return mHasBeenDecoded; } + + /** + * Returns true if this image has ever requested a decode before. + */ + bool GetHasRequestedDecode() { return mHasRequestedDecode; } + + /** + * Returns true if this image has been discarded and a decoded has not yet + * been created to redecode it. + */ + bool IsDiscarded() { return mDiscarded; } + + /** + * Sets the composited frame as valid or invalid. + */ + void SetCompositedFrameInvalid(bool aInvalid) { + MOZ_ASSERT(!aInvalid || + StaticPrefs::image_mem_animated_discardable_AtStartup()); + mCompositedFrameInvalid = aInvalid; + } + + /** + * Returns whether the composited frame is valid to draw to the screen. + */ + bool GetCompositedFrameInvalid() { return mCompositedFrameInvalid; } + + /** + * Returns whether the image is currently full decoded.. + */ + bool GetIsCurrentlyDecoded() { return mIsCurrentlyDecoded; } + + /** + * Call when you need to re-start animating. Ensures we start from the first + * frame. + */ + void ResetAnimation(); + + /** + * The animation mode of the image. + * + * Constants defined in imgIContainer.idl. + */ + void SetAnimationMode(uint16_t aAnimationMode); + + /// Update the number of frames of animation this image is known to have. + void UpdateKnownFrameCount(uint32_t aFrameCount); + + /// @return the number of frames of animation we know about so far. + uint32_t KnownFrameCount() const { return mFrameCount; } + + /// @return the number of frames this animation has, if we know for sure. + /// (In other words, if decoding is finished.) Otherwise, returns Nothing(). + Maybe FrameCount() const; + + /** + * Get or set the area of the image to invalidate when we loop around to the + * first frame. + */ + void SetFirstFrameRefreshArea(const gfx::IntRect& aRefreshArea); + gfx::IntRect FirstFrameRefreshArea() const { return mFirstFrameRefreshArea; } + + /** + * If the animation frame time has not yet been set, set it to + * TimeStamp::Now(). + */ + void InitAnimationFrameTimeIfNecessary(); + + /** + * Set the animation frame time to @aTime. + */ + void SetAnimationFrameTime(const TimeStamp& aTime); + + /** + * Set the animation frame time to @aTime if we are configured to stop the + * animation when not visible and aTime is later than the current time. + * Returns true if the time was updated, else false. + */ + bool MaybeAdvanceAnimationFrameTime(const TimeStamp& aTime); + + /** + * The current frame we're on, from 0 to (numFrames - 1). + */ + uint32_t GetCurrentAnimationFrameIndex() const; + + /* + * Set number of times to loop the image. + * @note -1 means loop forever. + */ + void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; } + int32_t LoopCount() const { return mLoopCount; } + + /// Set the @aLength of a single loop through this image. + void SetLoopLength(FrameTimeout aLength) { mLoopLength = Some(aLength); } + + /** + * @return the length of a single loop of this image. If this image is not + * finished decoding, is not animated, or it is animated but does not loop, + * returns FrameTimeout::Forever(). + */ + FrameTimeout LoopLength() const; + + /* + * Get or set the timeout for the first frame. This is used to allow animation + * scheduling even before a full decode runs for this image. + */ + void SetFirstFrameTimeout(FrameTimeout aTimeout) { + mFirstFrameTimeout = aTimeout; + } + FrameTimeout FirstFrameTimeout() const { return mFirstFrameTimeout; } + + private: + friend class FrameAnimator; + + //! Area of the first frame that needs to be redrawn on subsequent loops. + gfx::IntRect mFirstFrameRefreshArea; + + //! the time that the animation advanced to the current frame + TimeStamp mCurrentAnimationFrameTime; + + //! The number of frames of animation this image has. + uint32_t mFrameCount; + + //! The current frame index we're on, in the range [0, mFrameCount). + uint32_t mCurrentAnimationFrameIndex; + + //! number of loops remaining before animation stops (-1 no stop) + int32_t mLoopRemainingCount; + + //! The total number of loops for the image. + int32_t mLoopCount; + + //! The length of a single loop through this image. + Maybe mLoopLength; + + //! The timeout for the first frame of this image. + FrameTimeout mFirstFrameTimeout; + + //! The animation mode of this image. Constants defined in imgIContainer. + uint16_t mAnimationMode; + + /** + * The following four bools (mHasBeenDecoded, mIsCurrentlyDecoded, + * mCompositedFrameInvalid, mDiscarded) track the state of the image with + * regards to decoding. They all start out false, including mDiscarded, + * because we want to treat being discarded differently from "not yet decoded + * for the first time". + * + * (When we are decoding the image for the first time we want to show the + * image at the speed of data coming in from the network or the speed + * specified in the image file, whichever is slower. But when redecoding we + * want to show nothing until the frame for the current time has been + * decoded. The prevents the user from seeing the image "fast forward" + * to the expected spot.) + * + * When the image is decoded for the first time mHasBeenDecoded and + * mIsCurrentlyDecoded get set to true. When the image is discarded + * mIsCurrentlyDecoded gets set to false, and mCompositedFrameInvalid + * & mDiscarded get set to true. When we create a decoder to redecode the + * image mDiscarded gets set to false. mCompositedFrameInvalid gets set to + * false when we are able to advance to the frame that should be showing + * for the current time. mIsCurrentlyDecoded gets set to true when the + * redecode finishes. + */ + + //! Whether this image has been decoded at least once. + bool mHasBeenDecoded; + + //! Whether this image has ever requested a decode. + bool mHasRequestedDecode; + + //! Whether this image is currently fully decoded. + bool mIsCurrentlyDecoded; + + //! Whether the composited frame is valid to draw to the screen, note that + //! the composited frame can exist and be filled with image data but not + //! valid to draw to the screen. + bool mCompositedFrameInvalid; + + //! Whether this image is currently discarded. Only set to true after the + //! image has been decoded at least once. + bool mDiscarded; +}; + +/** + * RefreshResult is used to let callers know how the state of the animation + * changed during a call to FrameAnimator::RequestRefresh(). + */ +struct RefreshResult { + RefreshResult() : mFrameAdvanced(false), mAnimationFinished(false) {} + + /// Merges another RefreshResult's changes into this RefreshResult. + void Accumulate(const RefreshResult& aOther) { + mFrameAdvanced = mFrameAdvanced || aOther.mFrameAdvanced; + mAnimationFinished = mAnimationFinished || aOther.mAnimationFinished; + mDirtyRect = mDirtyRect.Union(aOther.mDirtyRect); + } + + // The region of the image that has changed. + gfx::IntRect mDirtyRect; + + // If true, we changed frames at least once. Note that, due to looping, we + // could still have ended up on the same frame! + bool mFrameAdvanced : 1; + + // Whether the animation has finished playing. + bool mAnimationFinished : 1; +}; + +class FrameAnimator { + public: + FrameAnimator(RasterImage* aImage, const gfx::IntSize& aSize) + : mImage(aImage), mSize(aSize) { + MOZ_COUNT_CTOR(FrameAnimator); + } + + MOZ_COUNTED_DTOR(FrameAnimator) + + /** + * Call when you need to re-start animating. Ensures we start from the first + * frame. + */ + void ResetAnimation(AnimationState& aState); + + /** + * Re-evaluate what frame we're supposed to be on, and do whatever blending + * is necessary to get us to that frame. + * + * Returns the result of that blending, including whether the current frame + * changed and what the resulting dirty rectangle is. + */ + RefreshResult RequestRefresh(AnimationState& aState, const TimeStamp& aTime); + + /** + * Get the full frame for the current frame of the animation (it may or may + * not have required compositing). It may not be available because it hasn't + * been decoded yet, in which case we return an empty LookupResult. + */ + LookupResult GetCompositedFrame(AnimationState& aState, bool aMarkUsed); + + private: // methods + /** + * Advances the animation. Typically, this will advance a single frame, but it + * may advance multiple frames. This may happen if we have infrequently + * "ticking" refresh drivers (e.g. in background tabs), or extremely short- + * lived animation frames. + * + * @param aTime the time that the animation should advance to. This will + * typically be <= TimeStamp::Now(). + * + * @param aCurrentFrame the currently displayed frame of the animation. If + * we advance, it will replace aCurrentFrame with the + * new current frame we advanced to. + * + * @returns a RefreshResult that shows whether the frame was successfully + * advanced, and its resulting dirty rect. + */ + RefreshResult AdvanceFrame(AnimationState& aState, DrawableSurface& aFrames, + RefPtr& aCurrentFrame, TimeStamp aTime); + + /** + * Get the time the frame we're currently displaying is supposed to end. + * + * In the error case (like if the requested frame is not currently + * decoded), returns None(). + */ + TimeStamp GetCurrentImgFrameEndTime(AnimationState& aState, + FrameTimeout aCurrentTimeout) const; + + private: // data + //! A weak pointer to our owning image. + RasterImage* mImage; + + //! The intrinsic size of the image. + gfx::IntSize mSize; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_FrameAnimator_h diff --git a/image/FrameTimeout.h b/image/FrameTimeout.h new file mode 100644 index 0000000000..8cc38e203a --- /dev/null +++ b/image/FrameTimeout.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_FrameTimeout_h +#define mozilla_image_FrameTimeout_h + +#include +#include "mozilla/Assertions.h" + +namespace mozilla { +namespace image { + +/** + * FrameTimeout wraps a frame timeout value (measured in milliseconds) after + * first normalizing it. This normalization is necessary because some tools + * generate incorrect frame timeout values which we nevertheless have to + * support. For this reason, code that deals with frame timeouts should always + * use a FrameTimeout value rather than the raw value from the image header. + */ +struct FrameTimeout { + /** + * @return a FrameTimeout of zero. This should be used only for math + * involving FrameTimeout values. You can't obtain a zero FrameTimeout from + * FromRawMilliseconds(). + */ + static FrameTimeout Zero() { return FrameTimeout(0); } + + /// @return an infinite FrameTimeout. + static FrameTimeout Forever() { return FrameTimeout(-1); } + + /// @return a FrameTimeout obtained by normalizing a raw timeout value. + static FrameTimeout FromRawMilliseconds(int32_t aRawMilliseconds) { + // Normalize all infinite timeouts to the same value. + if (aRawMilliseconds < 0) { + return FrameTimeout::Forever(); + } + + // Very small timeout values are problematic for two reasons: we don't want + // to burn energy redrawing animated images extremely fast, and broken tools + // generate these values when they actually want a "default" value, so such + // images won't play back right without normalization. For some context, + // see bug 890743, bug 125137, bug 139677, and bug 207059. The historical + // behavior of IE and Opera was: + // IE 6/Win: + // 10 - 50ms is normalized to 100ms. + // >50ms is used unnormalized. + // Opera 7 final/Win: + // 10ms is normalized to 100ms. + // >10ms is used unnormalized. + if (aRawMilliseconds >= 0 && aRawMilliseconds <= 10) { + return FrameTimeout(100); + } + + // The provided timeout value is OK as-is. + return FrameTimeout(aRawMilliseconds); + } + + bool operator==(const FrameTimeout& aOther) const { + return mTimeout == aOther.mTimeout; + } + + bool operator!=(const FrameTimeout& aOther) const { + return !(*this == aOther); + } + + FrameTimeout operator+(const FrameTimeout& aOther) { + if (*this == Forever() || aOther == Forever()) { + return Forever(); + } + + return FrameTimeout(mTimeout + aOther.mTimeout); + } + + FrameTimeout& operator+=(const FrameTimeout& aOther) { + *this = *this + aOther; + return *this; + } + + /** + * @return this FrameTimeout's value in milliseconds. Illegal to call on a + * an infinite FrameTimeout value. + */ + uint32_t AsMilliseconds() const { + if (*this == Forever()) { + MOZ_ASSERT_UNREACHABLE( + "Calling AsMilliseconds() on an infinite FrameTimeout"); + return 100; // Fail to something sane. + } + + return uint32_t(mTimeout); + } + + /** + * @return this FrameTimeout value encoded so that non-negative values + * represent a timeout in milliseconds, and -1 represents an infinite + * timeout. + * + * XXX(seth): This is a backwards compatibility hack that should be removed. + */ + int32_t AsEncodedValueDeprecated() const { return mTimeout; } + + private: + explicit FrameTimeout(int32_t aTimeout) : mTimeout(aTimeout) {} + + int32_t mTimeout; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_FrameTimeout_h diff --git a/image/FrozenImage.cpp b/image/FrozenImage.cpp new file mode 100644 index 0000000000..bb82aaedd6 --- /dev/null +++ b/image/FrozenImage.cpp @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FrozenImage.h" + +namespace mozilla { + +using namespace gfx; +using layers::ImageContainer; + +namespace image { + +void FrozenImage::IncrementAnimationConsumers() { + // Do nothing. This will prevent animation from starting if there are no other + // instances of this image. +} + +void FrozenImage::DecrementAnimationConsumers() { + // Do nothing. +} + +NS_IMETHODIMP +FrozenImage::GetAnimated(bool* aAnimated) { + bool dummy; + nsresult rv = InnerImage()->GetAnimated(&dummy); + if (NS_SUCCEEDED(rv)) { + *aAnimated = false; + } + return rv; +} + +NS_IMETHODIMP_(already_AddRefed) +FrozenImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + return InnerImage()->GetFrame(FRAME_FIRST, aFlags); +} + +NS_IMETHODIMP_(already_AddRefed) +FrozenImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + return InnerImage()->GetFrameAtSize(aSize, FRAME_FIRST, aFlags); +} + +bool FrozenImage::IsNonAnimated() const { + // We usually don't create frozen images for non-animated images, but it might + // happen if we don't have enough data at the time of the creation to + // determine whether the image is actually animated. + bool animated = false; + return NS_SUCCEEDED(InnerImage()->GetAnimated(&animated)) && !animated; +} + +NS_IMETHODIMP_(bool) +FrozenImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + if (IsNonAnimated()) { + return InnerImage()->IsImageContainerAvailable(aRenderer, aFlags); + } + return false; +} + +NS_IMETHODIMP_(ImgDrawResult) +FrozenImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + if (IsNonAnimated()) { + return InnerImage()->GetImageProvider(aRenderer, aSize, aSVGContext, + aRegion, aFlags, aProvider); + } + + // XXX(seth): GetImageContainer does not currently support anything but the + // current frame. We work around this by always returning null, but if it ever + // turns out that FrozenImage is widely used on codepaths that can actually + // benefit from GetImageContainer, it would be a good idea to fix that method + // for performance reasons. + return ImgDrawResult::NOT_SUPPORTED; +} + +NS_IMETHODIMP_(ImgDrawResult) +FrozenImage::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, + uint32_t /* aWhichFrame - ignored */, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + return InnerImage()->Draw(aContext, aSize, aRegion, FRAME_FIRST, + aSamplingFilter, aSVGContext, aFlags, aOpacity); +} + +NS_IMETHODIMP +FrozenImage::StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) { + return InnerImage()->StartDecoding(aFlags, FRAME_FIRST); +} + +bool FrozenImage::StartDecodingWithResult(uint32_t aFlags, + uint32_t aWhichFrame) { + return InnerImage()->StartDecodingWithResult(aFlags, FRAME_FIRST); +} + +bool FrozenImage::HasDecodedPixels() { + return InnerImage()->HasDecodedPixels(); +} + +imgIContainer::DecodeResult FrozenImage::RequestDecodeWithResult( + uint32_t aFlags, uint32_t aWhichFrame) { + return InnerImage()->RequestDecodeWithResult(aFlags, FRAME_FIRST); +} + +NS_IMETHODIMP +FrozenImage::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) { + return InnerImage()->RequestDecodeForSize(aSize, aFlags, FRAME_FIRST); +} + +NS_IMETHODIMP_(void) +FrozenImage::RequestRefresh(const TimeStamp& aTime) { + // Do nothing. +} + +NS_IMETHODIMP +FrozenImage::GetAnimationMode(uint16_t* aAnimationMode) { + *aAnimationMode = kNormalAnimMode; + return NS_OK; +} + +NS_IMETHODIMP +FrozenImage::SetAnimationMode(uint16_t aAnimationMode) { + // Do nothing. + return NS_OK; +} + +NS_IMETHODIMP +FrozenImage::ResetAnimation() { + // Do nothing. + return NS_OK; +} + +NS_IMETHODIMP_(float) +FrozenImage::GetFrameIndex(uint32_t aWhichFrame) { + MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument"); + return 0; +} + +} // namespace image +} // namespace mozilla diff --git a/image/FrozenImage.h b/image/FrozenImage.h new file mode 100644 index 0000000000..6f580c826c --- /dev/null +++ b/image/FrozenImage.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_FrozenImage_h +#define mozilla_image_FrozenImage_h + +#include "ImageWrapper.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +namespace image { + +/** + * An Image wrapper that disables animation, freezing the image at its first + * frame. It does this using two strategies. If this is the only instance of the + * image, animation will never start, because IncrementAnimationConsumers is + * ignored. If there is another instance that is animated, that's still OK, + * because any imgIContainer method that is affected by animation gets its + * aWhichFrame argument set to FRAME_FIRST when it passes through FrozenImage. + * + * XXX(seth): There a known (performance, not correctness) issue with + * GetImageContainer. See the comments for that method for more information. + */ +class FrozenImage : public ImageWrapper { + typedef gfx::SourceSurface SourceSurface; + + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(FrozenImage, ImageWrapper) + + virtual void IncrementAnimationConsumers() override; + virtual void DecrementAnimationConsumers() override; + + bool IsNonAnimated() const; + + NS_IMETHOD GetAnimated(bool* aAnimated) override; + NS_IMETHOD_(already_AddRefed) + GetFrame(uint32_t aWhichFrame, uint32_t aFlags) override; + NS_IMETHOD_(already_AddRefed) + GetFrameAtSize(const gfx::IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) override; + NS_IMETHOD_(bool) + IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) override; + NS_IMETHOD_(ImgDrawResult) + GetImageProvider(WindowRenderer* aRenderer, const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, uint32_t aFlags, + WebRenderImageProvider** aProvider) override; + NS_IMETHOD_(ImgDrawResult) + Draw(gfxContext* aContext, const nsIntSize& aSize, const ImageRegion& aRegion, + uint32_t aWhichFrame, gfx::SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) override; + NS_IMETHOD StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) override; + NS_IMETHOD_(bool) + StartDecodingWithResult(uint32_t aFlags, uint32_t aWhichFrame) override; + NS_IMETHOD_(bool) + HasDecodedPixels() override; + NS_IMETHOD_(DecodeResult) + RequestDecodeWithResult(uint32_t aFlags, uint32_t aWhichFrame) override; + NS_IMETHOD RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) override; + NS_IMETHOD_(void) RequestRefresh(const TimeStamp& aTime) override; + NS_IMETHOD GetAnimationMode(uint16_t* aAnimationMode) override; + NS_IMETHOD SetAnimationMode(uint16_t aAnimationMode) override; + NS_IMETHOD ResetAnimation() override; + NS_IMETHOD_(float) GetFrameIndex(uint32_t aWhichFrame) override; + + protected: + explicit FrozenImage(Image* aImage) : ImageWrapper(aImage) {} + virtual ~FrozenImage() {} + + private: + friend class ImageOps; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_FrozenImage_h diff --git a/image/ICOFileHeaders.h b/image/ICOFileHeaders.h new file mode 100644 index 0000000000..a47919b91a --- /dev/null +++ b/image/ICOFileHeaders.h @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ICOFileHeaders_h +#define mozilla_image_ICOFileHeaders_h + +namespace mozilla { +namespace image { + +#define ICONFILEHEADERSIZE 6 +#define ICODIRENTRYSIZE 16 +#define PNGSIGNATURESIZE 8 +#define BMPFILEHEADERSIZE 14 + +/** + * The header that comes right at the start of an icon file. (This + * corresponds to the Windows ICONDIR structure.) + */ +struct IconFileHeader { + /** + * Must be set to 0; + */ + uint16_t mReserved; + /** + * 1 for icon (.ICO) image (or 2 for cursor (.CUR) image (icon with the + * addition of a hotspot), but we don't support cursor). + */ + uint16_t mType; + /** + * The number of BMP/PNG images contained in the icon file. + */ + uint16_t mCount; +}; + +/** + * For each BMP/PNG image that the icon file contains there must be a + * corresponding icon dir entry. (This corresponds to the Windows + * ICONDIRENTRY structure.) These entries are encoded directly after the + * IconFileHeader. + */ +struct IconDirEntry { + uint8_t mWidth; + uint8_t mHeight; + /** + * The number of colors in the color palette of the BMP/PNG that this dir + * entry corresponds to, or 0 if the image does not use a color palette. + */ + uint8_t mColorCount; + /** + * Should be set to 0. + */ + uint8_t mReserved; + union { + uint16_t mPlanes; // ICO + uint16_t mXHotspot; // CUR + }; + union { + uint16_t mBitCount; // ICO (bits per pixel) + uint16_t mYHotspot; // CUR + }; + /** + * "bytes in resource" is the length of the encoded BMP/PNG that this dir + * entry corresponds to. + */ + uint32_t mBytesInRes; + /** + * The offset of the start of the encoded BMP/PNG that this dir entry + * corresponds to (from the start of the icon file). + */ + uint32_t mImageOffset; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ICOFileHeaders_h diff --git a/image/IDecodingTask.cpp b/image/IDecodingTask.cpp new file mode 100644 index 0000000000..1816c9056b --- /dev/null +++ b/image/IDecodingTask.cpp @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "IDecodingTask.h" + +#include "nsThreadUtils.h" +#include "mozilla/AppShutdown.h" + +#include "Decoder.h" +#include "DecodePool.h" +#include "RasterImage.h" +#include "SurfaceCache.h" + +namespace mozilla { + +using gfx::IntRect; + +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// Helpers for sending notifications to the image associated with a decoder. +/////////////////////////////////////////////////////////////////////////////// + +void IDecodingTask::EnsureHasEventTarget(NotNull aImage) { + if (!mEventTarget) { + // We determine the event target as late as possible, at the first dispatch + // time, because the observers bound to an imgRequest will affect it. + // We cache it rather than query for the event target each time because the + // event target can change. We don't want to risk events being executed in + // a different order than they are dispatched, which can happen if we + // selected scheduler groups which have no ordering guarantees relative to + // each other (e.g. it moves from scheduler group A for doc group DA to + // scheduler group B for doc group DB due to changing observers -- if we + // dispatched the first event on A, and the second on B, we don't know which + // will execute first.) + RefPtr tracker = aImage->GetProgressTracker(); + if (tracker) { + mEventTarget = tracker->GetEventTarget(); + } else { + mEventTarget = GetMainThreadSerialEventTarget(); + } + } +} + +bool IDecodingTask::IsOnEventTarget() const { + // This is essentially equivalent to NS_IsOnMainThread() because all of the + // event targets are for the main thread (although perhaps with a different + // label / scheduler group). The observers in ProgressTracker may have + // different event targets from this, so this is just a best effort guess. + bool current = false; + mEventTarget->IsOnCurrentThread(¤t); + return current; +} + +void IDecodingTask::NotifyProgress(NotNull aImage, + NotNull aDecoder) { + MOZ_ASSERT(aDecoder->HasProgress() && !aDecoder->IsMetadataDecode()); + EnsureHasEventTarget(aImage); + + // Capture the decoder's state. If we need to notify asynchronously, it's + // important that we don't wait until the lambda actually runs to capture the + // state that we're going to notify. That would both introduce data races on + // the decoder's state and cause inconsistencies between the NotifyProgress() + // calls we make off-main-thread and the notifications that RasterImage + // actually receives, which would cause bugs. + Progress progress = aDecoder->TakeProgress(); + OrientedIntRect invalidRect = aDecoder->TakeInvalidRect(); + Maybe frameCount = aDecoder->TakeCompleteFrameCount(); + DecoderFlags decoderFlags = aDecoder->GetDecoderFlags(); + SurfaceFlags surfaceFlags = aDecoder->GetSurfaceFlags(); + + // Synchronously notify if we can. + if (IsOnEventTarget() && !(decoderFlags & DecoderFlags::ASYNC_NOTIFY)) { + aImage->NotifyProgress(progress, invalidRect, frameCount, decoderFlags, + surfaceFlags); + return; + } + + // Don't try to dispatch after shutdown, we'll just leak the runnable. + if (NS_WARN_IF( + AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads))) { + return; + } + + // We're forced to notify asynchronously. + NotNull> image = aImage; + mEventTarget->Dispatch(CreateRenderBlockingRunnable(NS_NewRunnableFunction( + "IDecodingTask::NotifyProgress", + [=]() -> void { + image->NotifyProgress(progress, invalidRect, + frameCount, decoderFlags, + surfaceFlags); + })), + NS_DISPATCH_NORMAL); +} + +void IDecodingTask::NotifyDecodeComplete(NotNull aImage, + NotNull aDecoder) { + MOZ_ASSERT(aDecoder->HasError() || !aDecoder->InFrame(), + "Decode complete in the middle of a frame?"); + EnsureHasEventTarget(aImage); + + // Capture the decoder's state. + DecoderFinalStatus finalStatus = aDecoder->FinalStatus(); + ImageMetadata metadata = aDecoder->GetImageMetadata(); + DecoderTelemetry telemetry = aDecoder->Telemetry(); + Progress progress = aDecoder->TakeProgress(); + OrientedIntRect invalidRect = aDecoder->TakeInvalidRect(); + Maybe frameCount = aDecoder->TakeCompleteFrameCount(); + DecoderFlags decoderFlags = aDecoder->GetDecoderFlags(); + SurfaceFlags surfaceFlags = aDecoder->GetSurfaceFlags(); + + // Synchronously notify if we can. + if (IsOnEventTarget() && !(decoderFlags & DecoderFlags::ASYNC_NOTIFY)) { + aImage->NotifyDecodeComplete(finalStatus, metadata, telemetry, progress, + invalidRect, frameCount, decoderFlags, + surfaceFlags); + return; + } + + // Don't try to dispatch after shutdown, we'll just leak the runnable. + if (NS_WARN_IF( + AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads))) { + return; + } + + // We're forced to notify asynchronously. + NotNull> image = aImage; + mEventTarget->Dispatch(CreateRenderBlockingRunnable(NS_NewRunnableFunction( + "IDecodingTask::NotifyDecodeComplete", + [=]() -> void { + image->NotifyDecodeComplete( + finalStatus, metadata, telemetry, progress, + invalidRect, frameCount, decoderFlags, + surfaceFlags); + })), + NS_DISPATCH_NORMAL); +} + +/////////////////////////////////////////////////////////////////////////////// +// IDecodingTask implementation. +/////////////////////////////////////////////////////////////////////////////// + +void IDecodingTask::Resume() { DecodePool::Singleton()->AsyncRun(this); } + +/////////////////////////////////////////////////////////////////////////////// +// MetadataDecodingTask implementation. +/////////////////////////////////////////////////////////////////////////////// + +MetadataDecodingTask::MetadataDecodingTask(NotNull aDecoder) + : mMutex("mozilla::image::MetadataDecodingTask"), mDecoder(aDecoder) { + MOZ_ASSERT(mDecoder->IsMetadataDecode(), + "Use DecodingTask for non-metadata decodes"); +} + +void MetadataDecodingTask::Run() { + MutexAutoLock lock(mMutex); + + LexerResult result = mDecoder->Decode(WrapNotNull(this)); + + if (result.is()) { + NotifyDecodeComplete(mDecoder->GetImage(), mDecoder); + return; // We're done. + } + + if (result == LexerResult(Yield::NEED_MORE_DATA)) { + // We can't make any more progress right now. We also don't want to report + // any progress, because it's important that metadata decode results are + // delivered atomically. The decoder itself will ensure that we get + // reenqueued when more data is available; just return for now. + return; + } + + MOZ_ASSERT_UNREACHABLE("Metadata decode yielded for an unexpected reason"); +} + +/////////////////////////////////////////////////////////////////////////////// +// AnonymousDecodingTask implementation. +/////////////////////////////////////////////////////////////////////////////// + +AnonymousDecodingTask::AnonymousDecodingTask(NotNull aDecoder, + bool aResumable) + : mDecoder(aDecoder), mResumable(aResumable) {} + +void AnonymousDecodingTask::Run() { + while (true) { + LexerResult result = mDecoder->Decode(WrapNotNull(this)); + + if (result.is()) { + return; // We're done. + } + + if (result == LexerResult(Yield::NEED_MORE_DATA)) { + // We can't make any more progress right now. Let the caller decide how to + // handle it. + return; + } + + // Right now we don't do anything special for other kinds of yields, so just + // keep working. + MOZ_ASSERT(result.is()); + } +} + +void AnonymousDecodingTask::Resume() { + // Anonymous decoders normally get all their data at once. We have tests + // where they don't; typically in these situations, the test re-runs them + // manually. However some tests want to verify Resume works, so they will + // explicitly request this behaviour. + if (mResumable) { + RefPtr self(this); + NS_DispatchToMainThread( + NS_NewRunnableFunction("image::AnonymousDecodingTask::Resume", + [self]() -> void { self->Run(); })); + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/IDecodingTask.h b/image/IDecodingTask.h new file mode 100644 index 0000000000..b3bce74757 --- /dev/null +++ b/image/IDecodingTask.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * An interface for tasks which can execute on the ImageLib DecodePool, and + * various implementations. + */ + +#ifndef mozilla_image_IDecodingTask_h +#define mozilla_image_IDecodingTask_h + +#include "imgFrame.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "nsIEventTarget.h" +#include "SourceBuffer.h" + +namespace mozilla { +namespace image { + +class Decoder; +class RasterImage; + +/// A priority hint that DecodePool can use when scheduling an IDecodingTask. +enum class TaskPriority : uint8_t { eLow, eHigh }; + +/** + * An interface for tasks which can execute on the ImageLib DecodePool. + */ +class IDecodingTask : public IResumable { + public: + /// Run the task. + virtual void Run() = 0; + + /// @return true if, given the option, this task prefers to run synchronously. + virtual bool ShouldPreferSyncRun() const = 0; + + /// @return a priority hint that DecodePool can use when scheduling this task. + virtual TaskPriority Priority() const = 0; + + /// A default implementation of IResumable which resubmits the task to the + /// DecodePool. Subclasses can override this if they need different behavior. + void Resume() override; + + protected: + virtual ~IDecodingTask() {} + + /// Notify @aImage of @aDecoder's progress. + void NotifyProgress(NotNull aImage, NotNull aDecoder); + + /// Notify @aImage that @aDecoder has finished. + void NotifyDecodeComplete(NotNull aImage, + NotNull aDecoder); + + private: + void EnsureHasEventTarget(NotNull aImage); + + bool IsOnEventTarget() const; + + nsCOMPtr mEventTarget; +}; + +/** + * An IDecodingTask implementation for metadata decodes of images. + */ +class MetadataDecodingTask final : public IDecodingTask { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MetadataDecodingTask, override) + + explicit MetadataDecodingTask(NotNull aDecoder); + + void Run() override; + + // Metadata decodes are very fast (since they only need to examine an image's + // header) so there's no reason to refuse to run them synchronously if the + // caller will allow us to. + bool ShouldPreferSyncRun() const override { return true; } + + // Metadata decodes run at the highest priority because they block layout and + // page load. + TaskPriority Priority() const override { return TaskPriority::eHigh; } + + private: + virtual ~MetadataDecodingTask() {} + + /// Mutex protecting access to mDecoder. + Mutex mMutex MOZ_UNANNOTATED; + + NotNull> mDecoder; +}; + +/** + * An IDecodingTask implementation for anonymous decoders - that is, decoders + * with no associated Image object. + */ +class AnonymousDecodingTask final : public IDecodingTask { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AnonymousDecodingTask, override) + + explicit AnonymousDecodingTask(NotNull aDecoder, bool aResumable); + + void Run() override; + + bool ShouldPreferSyncRun() const override { return true; } + TaskPriority Priority() const override { return TaskPriority::eLow; } + + void Resume() override; + + private: + virtual ~AnonymousDecodingTask() {} + + NotNull> mDecoder; + bool mResumable; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_IDecodingTask_h diff --git a/image/IProgressObserver.h b/image/IProgressObserver.h new file mode 100644 index 0000000000..c4b16f70de --- /dev/null +++ b/image/IProgressObserver.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_IProgressObserver_h +#define mozilla_image_IProgressObserver_h + +#include "mozilla/WeakPtr.h" +#include "nsISupports.h" +#include "nsRect.h" + +class nsIEventTarget; + +namespace mozilla { +namespace image { + +/** + * An interface for observing changes to image state, as reported by + * ProgressTracker. + * + * This is the ImageLib-internal version of imgINotificationObserver, + * essentially, with implementation details that code outside of ImageLib + * shouldn't see. + * + * XXX(seth): It's preferable to avoid adding anything to this interface if + * possible. In the long term, it would be ideal to get to a place where we can + * just use the imgINotificationObserver interface internally as well. + */ +class IProgressObserver : public SupportsWeakPtr { + public: + // Subclasses may or may not be XPCOM classes, so we just require that they + // implement AddRef and Release. + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + // imgINotificationObserver methods: + virtual void Notify(int32_t aType, const nsIntRect* aRect = nullptr) = 0; + virtual void OnLoadComplete(bool aLastPart) = 0; + + // Other, internal-only methods: + virtual void SetHasImage() = 0; + virtual bool NotificationsDeferred() const = 0; + virtual void MarkPendingNotify() = 0; + virtual void ClearPendingNotify() = 0; + + virtual already_AddRefed GetEventTarget() const { + return nullptr; + } + + protected: + virtual ~IProgressObserver() = default; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_IProgressObserver_h diff --git a/image/ISurfaceProvider.h b/image/ISurfaceProvider.h new file mode 100644 index 0000000000..03ab713238 --- /dev/null +++ b/image/ISurfaceProvider.h @@ -0,0 +1,346 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * An interface for objects which can either store a surface or dynamically + * generate one, and various implementations. + */ + +#ifndef mozilla_image_ISurfaceProvider_h +#define mozilla_image_ISurfaceProvider_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/NotNull.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/image/WebRenderImageProvider.h" + +#include "imgFrame.h" +#include "SurfaceCache.h" + +namespace mozilla { +namespace image { + +class CachedSurface; +class DrawableSurface; + +/** + * An interface for objects which can either store a surface or dynamically + * generate one. + */ +class ISurfaceProvider : public WebRenderImageProvider { + public: + // Subclasses may or may not be XPCOM classes, so we just require that they + // implement AddRef and Release. + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + /// @return key data used for identifying which image this ISurfaceProvider is + /// associated with in the surface cache. + ImageKey GetImageKey() const { return mImageKey; } + + /// @return key data used to uniquely identify this ISurfaceProvider's cache + /// entry in the surface cache. + const SurfaceKey& GetSurfaceKey() const { return mSurfaceKey; } + + /// @return a drawable reference to a surface. + DrawableSurface Surface(); + + /// @return true if DrawableRef() will return a completely decoded surface. + virtual bool IsFinished() const = 0; + + /// @return true if the underlying decoder is currently fully decoded. For + /// animated images, this means that at least every frame has been decoded + /// at least once. It does not guarantee that all of the frames are present, + /// as the surface provider has the option to discard as it deems necessary. + virtual bool IsFullyDecoded() const { return IsFinished(); } + + /// @return the number of bytes of memory this ISurfaceProvider is expected to + /// require. Optimizations may result in lower real memory usage. Trivial + /// overhead is ignored. Because this value is used in bookkeeping, it's + /// important that it be constant over the lifetime of this object. + virtual size_t LogicalSizeInBytes() const = 0; + + typedef imgFrame::AddSizeOfCbData AddSizeOfCbData; + typedef imgFrame::AddSizeOfCb AddSizeOfCb; + + /// @return the actual number of bytes of memory this ISurfaceProvider is + /// using. May vary over the lifetime of the ISurfaceProvider. The default + /// implementation is appropriate for static ISurfaceProviders. + virtual void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) { + DrawableFrameRef ref = DrawableRef(/* aFrame = */ 0); + if (!ref) { + return; + } + + ref->AddSizeOfExcludingThis(aMallocSizeOf, aCallback); + } + + virtual void Reset() {} + virtual void Advance(size_t aFrame) {} + virtual bool MayAdvance() const { return false; } + virtual void MarkMayAdvance() {} + + /// @return the availability state of this ISurfaceProvider, which indicates + /// whether DrawableRef() could successfully return a surface. Should only be + /// called from SurfaceCache code as it relies on SurfaceCache for + /// synchronization. + AvailabilityState& Availability() { return mAvailability; } + const AvailabilityState& Availability() const { return mAvailability; } + + protected: + ISurfaceProvider(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + AvailabilityState aAvailability) + : WebRenderImageProvider(aImageKey), + mImageKey(aImageKey), + mSurfaceKey(aSurfaceKey), + mAvailability(aAvailability) { + MOZ_ASSERT(aImageKey, "Must have a valid image key"); + } + + virtual ~ISurfaceProvider() {} + + /// @return an eagerly computed drawable reference to a surface. For + /// dynamically generated animation surfaces, @aFrame specifies the 0-based + /// index of the desired frame. + virtual DrawableFrameRef DrawableRef(size_t aFrame) = 0; + + /// @return an imgFrame at the 0-based index of the desired frame, as + /// specified by @aFrame. Only applies for animated images. + virtual already_AddRefed GetFrame(size_t aFrame) { + MOZ_ASSERT_UNREACHABLE("Surface provider does not support direct access!"); + return nullptr; + } + + /// @return true if this ISurfaceProvider is locked. (@see SetLocked()) + /// Should only be called from SurfaceCache code as it relies on SurfaceCache + /// for synchronization. + virtual bool IsLocked() const = 0; + + /// If @aLocked is true, hint that this ISurfaceProvider is in use and it + /// should avoid releasing its resources. Should only be called from + /// SurfaceCache code as it relies on SurfaceCache for synchronization. + virtual void SetLocked(bool aLocked) = 0; + + private: + friend class CachedSurface; + friend class DrawableSurface; + + const ImageKey mImageKey; + const SurfaceKey mSurfaceKey; + AvailabilityState mAvailability; +}; + +/** + * A reference to a surface (stored in an imgFrame) that holds the surface in + * memory, guaranteeing that it can be drawn. If you have a DrawableSurface + * |surf| and |if (surf)| returns true, then calls to |surf->Draw()| and + * |surf->GetSourceSurface()| are guaranteed to succeed. + * + * Note that the surface may be computed lazily, so a DrawableSurface should not + * be dereferenced (i.e., operator->() should not be called) until you're + * sure that you want to draw it. + */ +class MOZ_STACK_CLASS DrawableSurface final { + public: + DrawableSurface() : mHaveSurface(false) {} + + explicit DrawableSurface(NotNull aProvider) + : mProvider(aProvider), mHaveSurface(true) {} + + DrawableSurface(DrawableSurface&& aOther) + : mDrawableRef(std::move(aOther.mDrawableRef)), + mProvider(std::move(aOther.mProvider)), + mHaveSurface(aOther.mHaveSurface) { + aOther.mHaveSurface = false; + } + + DrawableSurface& operator=(DrawableSurface&& aOther) { + MOZ_ASSERT(this != &aOther, "Self-moves are prohibited"); + mDrawableRef = std::move(aOther.mDrawableRef); + mProvider = std::move(aOther.mProvider); + mHaveSurface = aOther.mHaveSurface; + aOther.mHaveSurface = false; + return *this; + } + + /** + * If this DrawableSurface is dynamically generated from an animation, attempt + * to seek to frame @aFrame, where @aFrame is a 0-based index into the frames + * of the animation. Otherwise, nothing will blow up at runtime, but we assert + * in debug builds, since calling this in an unexpected situation probably + * indicates a bug. + * + * @return a successful result if we could obtain frame @aFrame. Note that + * |mHaveSurface| being true means that we're guaranteed to have *some* frame, + * so the caller can dereference this DrawableSurface even if Seek() fails, + * but while nothing will blow up, the frame won't be the one they expect. + */ + nsresult Seek(size_t aFrame) { + MOZ_ASSERT(mHaveSurface, "Trying to seek an empty DrawableSurface?"); + + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to seek a static DrawableSurface?"); + return NS_ERROR_FAILURE; + } + + mDrawableRef = mProvider->DrawableRef(aFrame); + + return mDrawableRef ? NS_OK : NS_ERROR_FAILURE; + } + + already_AddRefed GetFrame(size_t aFrame) { + MOZ_ASSERT(mHaveSurface, "Trying to get on an empty DrawableSurface?"); + + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to get on a static DrawableSurface?"); + return nullptr; + } + + return mProvider->GetFrame(aFrame); + } + + void Reset() { + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to reset a static DrawableSurface?"); + return; + } + + mProvider->Reset(); + } + + void Advance(size_t aFrame) { + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to advance a static DrawableSurface?"); + return; + } + + mProvider->Advance(aFrame); + } + + bool MayAdvance() const { + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to advance a static DrawableSurface?"); + return false; + } + + return mProvider->MayAdvance(); + } + + void MarkMayAdvance() { + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE("Trying to advance a static DrawableSurface?"); + return; + } + + mProvider->MarkMayAdvance(); + } + + bool IsFullyDecoded() const { + if (!mProvider) { + MOZ_ASSERT_UNREACHABLE( + "Trying to check decoding state of a static DrawableSurface?"); + return false; + } + + return mProvider->IsFullyDecoded(); + } + + void TakeProvider(WebRenderImageProvider** aOutProvider) { + mProvider.forget(aOutProvider); + } + + explicit operator bool() const { return mHaveSurface; } + imgFrame* operator->() { return DrawableRef().get(); } + + private: + DrawableSurface(const DrawableSurface& aOther) = delete; + DrawableSurface& operator=(const DrawableSurface& aOther) = delete; + + DrawableFrameRef& DrawableRef() { + MOZ_ASSERT(mHaveSurface); + + // If we weren't created with a DrawableFrameRef directly, we should've been + // created with an ISurfaceProvider which can give us one. Note that if + // Seek() has been called, we'll already have a DrawableFrameRef, so we + // won't need to get one here. + if (!mDrawableRef) { + MOZ_ASSERT(mProvider); + mDrawableRef = mProvider->DrawableRef(/* aFrame = */ 0); + } + + MOZ_ASSERT(mDrawableRef); + return mDrawableRef; + } + + DrawableFrameRef mDrawableRef; + RefPtr mProvider; + bool mHaveSurface; +}; + +// Surface() is implemented here so that DrawableSurface's definition is +// visible. +inline DrawableSurface ISurfaceProvider::Surface() { + return DrawableSurface(WrapNotNull(this)); +} + +/** + * An ISurfaceProvider that stores a single surface. + */ +class SimpleSurfaceProvider final : public ISurfaceProvider { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SimpleSurfaceProvider, override) + + SimpleSurfaceProvider(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + NotNull aSurface) + : ISurfaceProvider(aImageKey, aSurfaceKey, + AvailabilityState::StartAvailable()), + mSurface(aSurface) { + MOZ_ASSERT(aSurfaceKey.Size() == mSurface->GetSize()); + } + + bool IsFinished() const override { return mSurface->IsFinished(); } + + size_t LogicalSizeInBytes() const override { + gfx::IntSize size = mSurface->GetSize(); + return size.width * size.height * mSurface->GetBytesPerPixel(); + } + + nsresult UpdateKey(layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + wr::ImageKey& aKey) override; + + protected: + DrawableFrameRef DrawableRef(size_t aFrame) override { + MOZ_ASSERT(aFrame == 0, + "Requesting an animation frame from a SimpleSurfaceProvider?"); + return mSurface->DrawableRef(); + } + + bool IsLocked() const override { return bool(mLockRef); } + + void SetLocked(bool aLocked) override { + if (aLocked == IsLocked()) { + return; // Nothing changed. + } + + // If we're locked, hold a DrawableFrameRef to |mSurface|, which will keep + // any volatile buffer it owns in memory. + mLockRef = aLocked ? mSurface->DrawableRef() : DrawableFrameRef(); + } + + private: + virtual ~SimpleSurfaceProvider() {} + + NotNull> mSurface; + DrawableFrameRef mLockRef; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ISurfaceProvider_h diff --git a/image/Image.cpp b/image/Image.cpp new file mode 100644 index 0000000000..16f754f490 --- /dev/null +++ b/image/Image.cpp @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Image.h" + +#include "imgRequest.h" +#include "WebRenderImageProvider.h" +#include "nsIObserverService.h" +#include "nsRefreshDriver.h" +#include "nsContentUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/gfx/Rect.h" +#include "mozilla/gfx/SourceSurfaceRawData.h" +#include "mozilla/Services.h" +#include "mozilla/SizeOfState.h" +#include "mozilla/TimeStamp.h" +// for Tie +#include "mozilla/layers/SharedSurfacesChild.h" + +namespace mozilla { +namespace image { + +WebRenderImageProvider::WebRenderImageProvider(const ImageResource* aImage) + : mProviderId(aImage->GetImageProviderId()) {} + +/* static */ ImageProviderId WebRenderImageProvider::AllocateProviderId() { + // Callable on all threads. + static Atomic sProviderId(0u); + return ++sProviderId; +} + +/////////////////////////////////////////////////////////////////////////////// +// Memory Reporting +/////////////////////////////////////////////////////////////////////////////// + +ImageMemoryCounter::ImageMemoryCounter(imgRequest* aRequest, + SizeOfState& aState, bool aIsUsed) + : mProgress(UINT32_MAX), + mType(UINT16_MAX), + mIsUsed(aIsUsed), + mHasError(false), + mValidating(false) { + MOZ_ASSERT(aRequest); + + // We don't have the image object yet, but we can get some information. + nsCOMPtr imageURL; + nsresult rv = aRequest->GetURI(getter_AddRefs(imageURL)); + if (NS_SUCCEEDED(rv) && imageURL) { + imageURL->GetSpec(mURI); + } + + mType = imgIContainer::TYPE_REQUEST; + mHasError = NS_FAILED(aRequest->GetImageErrorCode()); + mValidating = !!aRequest->GetValidator(); + + RefPtr tracker = aRequest->GetProgressTracker(); + if (tracker) { + mProgress = tracker->GetProgress(); + } +} + +ImageMemoryCounter::ImageMemoryCounter(imgRequest* aRequest, Image* aImage, + SizeOfState& aState, bool aIsUsed) + : mProgress(UINT32_MAX), + mType(UINT16_MAX), + mIsUsed(aIsUsed), + mHasError(false), + mValidating(false) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aImage); + + // Extract metadata about the image. + nsCOMPtr imageURL(aImage->GetURI()); + if (imageURL) { + imageURL->GetSpec(mURI); + } + + int32_t width = 0; + int32_t height = 0; + aImage->GetWidth(&width); + aImage->GetHeight(&height); + mIntrinsicSize.SizeTo(width, height); + + mType = aImage->GetType(); + mHasError = aImage->HasError(); + mValidating = !!aRequest->GetValidator(); + + RefPtr tracker = aImage->GetProgressTracker(); + if (tracker) { + mProgress = tracker->GetProgress(); + } + + // Populate memory counters for source and decoded data. + mValues.SetSource(aImage->SizeOfSourceWithComputedFallback(aState)); + aImage->CollectSizeOfSurfaces(mSurfaces, aState.mMallocSizeOf); + + // Compute totals. + for (const SurfaceMemoryCounter& surfaceCounter : mSurfaces) { + mValues += surfaceCounter.Values(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Image Base Types +/////////////////////////////////////////////////////////////////////////////// + +bool ImageResource::GetSpecTruncatedTo1k(nsCString& aSpec) const { + static const size_t sMaxTruncatedLength = 1024; + + mURI->GetSpec(aSpec); + if (sMaxTruncatedLength >= aSpec.Length()) { + return true; + } + + aSpec.Truncate(sMaxTruncatedLength); + return false; +} + +void ImageResource::CollectSizeOfSurfaces( + nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const { + SurfaceCache::CollectSizeOfSurfaces(ImageKey(this), aCounters, aMallocSizeOf); +} + +// Constructor +ImageResource::ImageResource(nsIURI* aURI) + : mURI(aURI), + mInnerWindowId(0), + mAnimationConsumers(0), + mAnimationMode(kNormalAnimMode), + mInitialized(false), + mAnimating(false), + mError(false), + mProviderId(WebRenderImageProvider::AllocateProviderId()) {} + +ImageResource::~ImageResource() { + // Ask our ProgressTracker to drop its weak reference to us. + mProgressTracker->ResetImage(); +} + +void ImageResource::IncrementAnimationConsumers() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread only to encourage serialization " + "with DecrementAnimationConsumers"); + mAnimationConsumers++; +} + +void ImageResource::DecrementAnimationConsumers() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread only to encourage serialization " + "with IncrementAnimationConsumers"); + MOZ_ASSERT(mAnimationConsumers >= 1, "Invalid no. of animation consumers!"); + mAnimationConsumers--; +} + +nsresult ImageResource::GetAnimationModeInternal(uint16_t* aAnimationMode) { + if (mError) { + return NS_ERROR_FAILURE; + } + + NS_ENSURE_ARG_POINTER(aAnimationMode); + + *aAnimationMode = mAnimationMode; + return NS_OK; +} + +nsresult ImageResource::SetAnimationModeInternal(uint16_t aAnimationMode) { + if (mError) { + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(aAnimationMode == kNormalAnimMode || + aAnimationMode == kDontAnimMode || + aAnimationMode == kLoopOnceAnimMode, + "Wrong Animation Mode is being set!"); + + mAnimationMode = aAnimationMode; + + return NS_OK; +} + +bool ImageResource::HadRecentRefresh(const TimeStamp& aTime) { + // Our threshold for "recent" is 1/2 of the default refresh-driver interval. + // This ensures that we allow for frame rates at least as fast as the + // refresh driver's default rate. + static TimeDuration recentThreshold = + TimeDuration::FromMilliseconds(nsRefreshDriver::DefaultInterval() / 2.0); + + if (!mLastRefreshTime.IsNull() && + aTime - mLastRefreshTime < recentThreshold) { + return true; + } + + // else, we can proceed with a refresh. + // But first, update our last refresh time: + mLastRefreshTime = aTime; + return false; +} + +void ImageResource::EvaluateAnimation() { + if (!mAnimating && ShouldAnimate()) { + nsresult rv = StartAnimation(); + mAnimating = NS_SUCCEEDED(rv); + } else if (mAnimating && !ShouldAnimate()) { + StopAnimation(); + } +} + +void ImageResource::SendOnUnlockedDraw(uint32_t aFlags) { + if (!mProgressTracker) { + return; + } + + if (!(aFlags & FLAG_ASYNC_NOTIFY)) { + mProgressTracker->OnUnlockedDraw(); + } else { + NotNull> image = WrapNotNull(this); + nsCOMPtr eventTarget = mProgressTracker->GetEventTarget(); + nsCOMPtr ev = NS_NewRunnableFunction( + "image::ImageResource::SendOnUnlockedDraw", [=]() -> void { + RefPtr tracker = image->GetProgressTracker(); + if (tracker) { + tracker->OnUnlockedDraw(); + } + }); + eventTarget->Dispatch(CreateRenderBlockingRunnable(ev.forget()), + NS_DISPATCH_NORMAL); + } +} + +#ifdef DEBUG +void ImageResource::NotifyDrawingObservers() { + if (!mURI || !NS_IsMainThread()) { + return; + } + + if (!mURI->SchemeIs("resource") && !mURI->SchemeIs("chrome")) { + return; + } + + // Record the image drawing for startup performance testing. + nsCOMPtr uri = mURI; + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "image::ImageResource::NotifyDrawingObservers", [uri]() { + nsCOMPtr obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Can't get an observer service handle"); + if (obs) { + nsAutoCString spec; + uri->GetSpec(spec); + obs->NotifyObservers(nullptr, "image-drawing", + NS_ConvertUTF8toUTF16(spec).get()); + } + })); +} +#endif + +} // namespace image +} // namespace mozilla diff --git a/image/Image.h b/image/Image.h new file mode 100644 index 0000000000..cd4ca926d0 --- /dev/null +++ b/image/Image.h @@ -0,0 +1,419 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_Image_h +#define mozilla_image_Image_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/SizeOfState.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "mozilla/TimeStamp.h" + +#include "gfx2DGlue.h" +#include "imgIContainer.h" +#include "ImageContainer.h" +#include "ImageRegion.h" +#include "LookupResult.h" +#include "nsStringFwd.h" +#include "ProgressTracker.h" +#include "SurfaceCache.h" +#include "WebRenderImageProvider.h" + +class imgRequest; +class nsIRequest; +class nsIInputStream; + +namespace mozilla { +namespace image { + +class Image; + +/////////////////////////////////////////////////////////////////////////////// +// Memory Reporting +/////////////////////////////////////////////////////////////////////////////// + +struct MemoryCounter { + MemoryCounter() + : mSource(0), + mDecodedHeap(0), + mDecodedNonHeap(0), + mDecodedUnknown(0), + mExternalHandles(0), + mFrameIndex(0), + mExternalId(0), + mSurfaceTypes(0) {} + + void SetSource(size_t aCount) { mSource = aCount; } + size_t Source() const { return mSource; } + void SetDecodedHeap(size_t aCount) { mDecodedHeap = aCount; } + size_t DecodedHeap() const { return mDecodedHeap; } + void SetDecodedNonHeap(size_t aCount) { mDecodedNonHeap = aCount; } + size_t DecodedNonHeap() const { return mDecodedNonHeap; } + void SetDecodedUnknown(size_t aCount) { mDecodedUnknown = aCount; } + size_t DecodedUnknown() const { return mDecodedUnknown; } + void SetExternalHandles(size_t aCount) { mExternalHandles = aCount; } + size_t ExternalHandles() const { return mExternalHandles; } + void SetFrameIndex(size_t aIndex) { mFrameIndex = aIndex; } + size_t FrameIndex() const { return mFrameIndex; } + void SetExternalId(uint64_t aId) { mExternalId = aId; } + uint64_t ExternalId() const { return mExternalId; } + void SetSurfaceTypes(uint32_t aTypes) { mSurfaceTypes = aTypes; } + uint32_t SurfaceTypes() const { return mSurfaceTypes; } + + MemoryCounter& operator+=(const MemoryCounter& aOther) { + mSource += aOther.mSource; + mDecodedHeap += aOther.mDecodedHeap; + mDecodedNonHeap += aOther.mDecodedNonHeap; + mDecodedUnknown += aOther.mDecodedUnknown; + mExternalHandles += aOther.mExternalHandles; + mSurfaceTypes |= aOther.mSurfaceTypes; + return *this; + } + + private: + size_t mSource; + size_t mDecodedHeap; + size_t mDecodedNonHeap; + size_t mDecodedUnknown; + size_t mExternalHandles; + size_t mFrameIndex; + uint64_t mExternalId; + uint32_t mSurfaceTypes; +}; + +enum class SurfaceMemoryCounterType { NORMAL, CONTAINER }; + +struct SurfaceMemoryCounter { + SurfaceMemoryCounter( + const SurfaceKey& aKey, bool aIsLocked, bool aCannotSubstitute, + bool aIsFactor2, bool aFinished, + SurfaceMemoryCounterType aType = SurfaceMemoryCounterType::NORMAL) + : mKey(aKey), + mType(aType), + mIsLocked(aIsLocked), + mCannotSubstitute(aCannotSubstitute), + mIsFactor2(aIsFactor2), + mFinished(aFinished) {} + + const SurfaceKey& Key() const { return mKey; } + MemoryCounter& Values() { return mValues; } + const MemoryCounter& Values() const { return mValues; } + SurfaceMemoryCounterType Type() const { return mType; } + bool IsLocked() const { return mIsLocked; } + bool CannotSubstitute() const { return mCannotSubstitute; } + bool IsFactor2() const { return mIsFactor2; } + bool IsFinished() const { return mFinished; } + + private: + const SurfaceKey mKey; + MemoryCounter mValues; + const SurfaceMemoryCounterType mType; + const bool mIsLocked; + const bool mCannotSubstitute; + const bool mIsFactor2; + const bool mFinished; +}; + +struct ImageMemoryCounter { + ImageMemoryCounter(imgRequest* aRequest, SizeOfState& aState, bool aIsUsed); + ImageMemoryCounter(imgRequest* aRequest, Image* aImage, SizeOfState& aState, + bool aIsUsed); + + nsCString& URI() { return mURI; } + const nsCString& URI() const { return mURI; } + const nsTArray& Surfaces() const { return mSurfaces; } + const gfx::IntSize IntrinsicSize() const { return mIntrinsicSize; } + const MemoryCounter& Values() const { return mValues; } + uint32_t Progress() const { return mProgress; } + uint16_t Type() const { return mType; } + bool IsUsed() const { return mIsUsed; } + bool HasError() const { return mHasError; } + bool IsValidating() const { return mValidating; } + + bool IsNotable() const { + // Errors or requests without images are always notable. + if (mHasError || mValidating || mProgress == UINT32_MAX || + mProgress & FLAG_HAS_ERROR || mType == imgIContainer::TYPE_REQUEST) { + return true; + } + + // Sufficiently large images are notable. + const size_t NotableThreshold = 16 * 1024; + size_t total = mValues.Source() + mValues.DecodedHeap() + + mValues.DecodedNonHeap() + mValues.DecodedUnknown(); + if (total >= NotableThreshold) { + return true; + } + + // Incomplete images are always notable as well; the odds of capturing + // mid-decode should be fairly low. + for (const auto& surface : mSurfaces) { + if (!surface.IsFinished()) { + return true; + } + } + + return false; + } + + private: + nsCString mURI; + nsTArray mSurfaces; + gfx::IntSize mIntrinsicSize; + MemoryCounter mValues; + uint32_t mProgress; + uint16_t mType; + const bool mIsUsed; + bool mHasError; + bool mValidating; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Image Base Types +/////////////////////////////////////////////////////////////////////////////// + +class Image : public imgIContainer { + public: + /** + * Flags for Image initialization. + * + * Meanings: + * + * INIT_FLAG_NONE: Lack of flags + * + * INIT_FLAG_DISCARDABLE: The container should be discardable + * + * INIT_FLAG_DECODE_IMMEDIATELY: The container should decode as soon as + * possible, regardless of what our heuristics say. + * + * INIT_FLAG_TRANSIENT: The container is likely to exist for only a short time + * before being destroyed. (For example, containers for + * multipart/x-mixed-replace image parts fall into this category.) If this + * flag is set, INIT_FLAG_DISCARDABLE and INIT_FLAG_DECODE_ONLY_ON_DRAW must + * not be set. + * + * INIT_FLAG_SYNC_LOAD: The container is being loaded synchronously, so + * it should avoid relying on async workers to get the container ready. + */ + static const uint32_t INIT_FLAG_NONE = 0x0; + static const uint32_t INIT_FLAG_DISCARDABLE = 0x1; + static const uint32_t INIT_FLAG_DECODE_IMMEDIATELY = 0x2; + static const uint32_t INIT_FLAG_TRANSIENT = 0x4; + static const uint32_t INIT_FLAG_SYNC_LOAD = 0x8; + + virtual already_AddRefed GetProgressTracker() = 0; + virtual void SetProgressTracker(ProgressTracker* aProgressTracker) {} + + /** + * The size, in bytes, occupied by the compressed source data of the image. + * If MallocSizeOf does not work on this platform, uses a fallback approach to + * ensure that something reasonable is always returned. + */ + virtual size_t SizeOfSourceWithComputedFallback( + SizeOfState& aState) const = 0; + + /** + * Collect an accounting of the memory occupied by the image's surfaces (which + * together make up its decoded data). Each surface is recorded as a separate + * SurfaceMemoryCounter, stored in @aCounters. + */ + virtual void CollectSizeOfSurfaces(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const = 0; + + virtual void IncrementAnimationConsumers() = 0; + virtual void DecrementAnimationConsumers() = 0; +#ifdef DEBUG + virtual uint32_t GetAnimationConsumers() = 0; +#endif + + /** + * Called from OnDataAvailable when the stream associated with the image has + * received new image data. The arguments are the same as OnDataAvailable's, + * but by separating this functionality into a different method we don't + * interfere with subclasses which wish to implement nsIStreamListener. + * + * Images should not do anything that could send out notifications until they + * have received their first OnImageDataAvailable notification; in + * particular, this means that instantiating decoders should be deferred + * until OnImageDataAvailable is called. + */ + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) = 0; + + /** + * Called from OnStopRequest when the image's underlying request completes. + * + * @param aRequest The completed request. + * @param aStatus A success or failure code. + * @param aLastPart Whether this is the final part of the underlying request. + */ + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aStatus, + bool aLastPart) = 0; + + /** + * Called when the SurfaceCache discards a surface belonging to this image. + */ + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) = 0; + + virtual void SetInnerWindowID(uint64_t aInnerWindowId) = 0; + virtual uint64_t InnerWindowID() const = 0; + + virtual bool HasError() = 0; + virtual void SetHasError() = 0; + + virtual nsIURI* GetURI() const = 0; + + NS_IMETHOD GetHotspotX(int32_t* aX) override { + *aX = 0; + return NS_OK; + } + NS_IMETHOD GetHotspotY(int32_t* aY) override { + *aY = 0; + return NS_OK; + } +}; + +class ImageResource : public Image { + public: + already_AddRefed GetProgressTracker() override { + RefPtr progressTracker = mProgressTracker; + MOZ_ASSERT(progressTracker); + return progressTracker.forget(); + } + + void SetProgressTracker(ProgressTracker* aProgressTracker) final { + MOZ_ASSERT(aProgressTracker); + MOZ_ASSERT(!mProgressTracker); + mProgressTracker = aProgressTracker; + } + + virtual void IncrementAnimationConsumers() override; + virtual void DecrementAnimationConsumers() override; +#ifdef DEBUG + virtual uint32_t GetAnimationConsumers() override { + return mAnimationConsumers; + } +#endif + + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override {} + + virtual void SetInnerWindowID(uint64_t aInnerWindowId) override { + mInnerWindowId = aInnerWindowId; + } + virtual uint64_t InnerWindowID() const override { return mInnerWindowId; } + + virtual bool HasError() override { return mError; } + virtual void SetHasError() override { mError = true; } + + /* + * Returns a non-AddRefed pointer to the URI associated with this image. + * Illegal to use off-main-thread. + */ + nsIURI* GetURI() const override { return mURI; } + + /* + * Should be called by its subclasses after they populate @aCounters so that + * we can cross reference against any of our ImageContainers that contain + * surfaces not in the cache. + */ + void CollectSizeOfSurfaces(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const override; + + ImageProviderId GetImageProviderId() const { return mProviderId; } + + protected: + explicit ImageResource(nsIURI* aURI); + ~ImageResource(); + + bool GetSpecTruncatedTo1k(nsCString& aSpec) const; + + // Shared functionality for implementors of imgIContainer. Every + // implementation of attribute animationMode should forward here. + nsresult GetAnimationModeInternal(uint16_t* aAnimationMode); + nsresult SetAnimationModeInternal(uint16_t aAnimationMode); + + /** + * Helper for RequestRefresh. + * + * If we've had a "recent" refresh (i.e. if this image is being used in + * multiple documents & some other document *just* called RequestRefresh() on + * this image with a timestamp close to aTime), this method returns true. + * + * Otherwise, this method updates mLastRefreshTime to aTime & returns false. + */ + bool HadRecentRefresh(const TimeStamp& aTime); + + /** + * Decides whether animation should or should not be happening, + * and makes sure the right thing is being done. + */ + virtual void EvaluateAnimation(); + + /** + * Extended by child classes, if they have additional + * conditions for being able to animate. + */ + virtual bool ShouldAnimate() { + return mAnimationConsumers > 0 && mAnimationMode != kDontAnimMode; + } + + virtual nsresult StartAnimation() = 0; + virtual nsresult StopAnimation() = 0; + + void SendOnUnlockedDraw(uint32_t aFlags); + +#ifdef DEBUG + // Records the image drawing for startup performance testing. + void NotifyDrawingObservers(); +#endif + + // Member data shared by all implementations of this abstract class + RefPtr mProgressTracker; + nsCOMPtr mURI; + TimeStamp mLastRefreshTime; + uint64_t mInnerWindowId; + uint32_t mAnimationConsumers; + uint16_t mAnimationMode; // Enum values in imgIContainer + bool mInitialized : 1; // Have we been initialized? + bool mAnimating : 1; // Are we currently animating? + bool mError : 1; // Error handling + + class MOZ_RAII AutoProfilerImagePaintMarker { + public: + explicit AutoProfilerImagePaintMarker(ImageResource* self) { + if (self->mURI && profiler_thread_is_being_profiled_for_markers()) { + mStartTime = TimeStamp::Now(); + static const size_t sMaxTruncatedLength = 1024; + mSpec = nsContentUtils::TruncatedURLForDisplay(self->mURI, + sMaxTruncatedLength); + } + } + + ~AutoProfilerImagePaintMarker() { + if (!mSpec.IsEmpty()) { + PROFILER_MARKER_TEXT("Image Paint", GRAPHICS, + MarkerTiming::IntervalUntilNowFrom(mStartTime), + mSpec); + } + } + + protected: + TimeStamp mStartTime; + nsAutoCString mSpec; + }; + + private: + ImageProviderId mProviderId; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_Image_h diff --git a/image/ImageBlocker.cpp b/image/ImageBlocker.cpp new file mode 100644 index 0000000000..eee07589f7 --- /dev/null +++ b/image/ImageBlocker.cpp @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageBlocker.h" +#include "nsIPermissionManager.h" +#include "nsContentUtils.h" +#include "mozilla/StaticPrefs_permissions.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::image; + +NS_IMPL_ISUPPORTS(ImageBlocker, nsIContentPolicy) + +NS_IMETHODIMP +ImageBlocker::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, int16_t* aShouldLoad) { + ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType(); + + *aShouldLoad = nsIContentPolicy::ACCEPT; + + if (!aContentLocation) { + // Bug 1720280: Ideally we should block the load, but to avoid a potential + // null pointer deref, we return early in this case. Please note that + // the ImageBlocker only applies about http/https loads anyway. + return NS_OK; + } + + // we only want to check http, https + // for chrome:// and resources and others, no need to check. + nsAutoCString scheme; + aContentLocation->GetScheme(scheme); + if (!scheme.LowerCaseEqualsLiteral("http") && + !scheme.LowerCaseEqualsLiteral("https")) { + return NS_OK; + } + + // Block loading images depending on the permissions.default.image pref. + if ((contentType == ExtContentPolicy::TYPE_IMAGE || + contentType == ExtContentPolicy::TYPE_IMAGESET) && + StaticPrefs::permissions_default_image() == + nsIPermissionManager::DENY_ACTION) { + NS_SetRequestBlockingReason( + aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_CONTENT_BLOCKED); + *aShouldLoad = nsIContentPolicy::REJECT_TYPE; + } + + return NS_OK; +} + +NS_IMETHODIMP +ImageBlocker::ShouldProcess(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + int16_t* aShouldProcess) { + // We block images at load level already, so those should not end up here. + *aShouldProcess = nsIContentPolicy::ACCEPT; + return NS_OK; +} diff --git a/image/ImageBlocker.h b/image/ImageBlocker.h new file mode 100644 index 0000000000..f5b03280be --- /dev/null +++ b/image/ImageBlocker.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageBlocker_h +#define mozilla_image_ImageBlocker_h + +#include "nsIContentPolicy.h" + +namespace mozilla { +namespace image { + +#define IMAGEBLOCKER_CONTRACTID "@mozilla.org/image-blocker-content-policy;1" +#define IMAGEBLOCKER_CID \ + { /* f6fcd651-164b-4416-b001-9c8c393fd93b */ \ + 0xf6fcd651, 0x164b, 0x4416, { \ + 0xb0, 0x01, 0x9c, 0x8c, 0x39, 0x3f, 0xd9, 0x3b \ + } \ + } + +class ImageBlocker final : public nsIContentPolicy { + ~ImageBlocker() = default; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageBlocker_h diff --git a/image/ImageCacheKey.cpp b/image/ImageCacheKey.cpp new file mode 100644 index 0000000000..7a5f78b70f --- /dev/null +++ b/image/ImageCacheKey.cpp @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageCacheKey.h" + +#include + +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "nsContentUtils.h" +#include "nsHashKeys.h" +#include "nsLayoutUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" + +namespace mozilla { + +using namespace dom; + +namespace image { + +ImageCacheKey::ImageCacheKey(nsIURI* aURI, CORSMode aCORSMode, + const OriginAttributes& aAttrs, + Document* aDocument) + : mURI(aURI), + mOriginAttributes(aAttrs), + mControlledDocument(GetSpecialCaseDocumentToken(aDocument)), + mIsolationKey(GetIsolationKey(aDocument, aURI)), + mCORSMode(aCORSMode) {} + +ImageCacheKey::ImageCacheKey(const ImageCacheKey& aOther) + : mURI(aOther.mURI), + mOriginAttributes(aOther.mOriginAttributes), + mControlledDocument(aOther.mControlledDocument), + mIsolationKey(aOther.mIsolationKey), + mHash(aOther.mHash), + mCORSMode(aOther.mCORSMode) {} + +ImageCacheKey::ImageCacheKey(ImageCacheKey&& aOther) + : mURI(std::move(aOther.mURI)), + mOriginAttributes(aOther.mOriginAttributes), + mControlledDocument(aOther.mControlledDocument), + mIsolationKey(aOther.mIsolationKey), + mHash(aOther.mHash), + mCORSMode(aOther.mCORSMode) {} + +bool ImageCacheKey::operator==(const ImageCacheKey& aOther) const { + // Don't share the image cache between a controlled document and anything + // else. + if (mControlledDocument != aOther.mControlledDocument) { + return false; + } + // Don't share the image cache between two top-level documents of different + // base domains. + if (!mIsolationKey.Equals(aOther.mIsolationKey, + nsCaseInsensitiveCStringComparator)) { + return false; + } + // The origin attributes always have to match. + if (mOriginAttributes != aOther.mOriginAttributes) { + return false; + } + + if (mCORSMode != aOther.mCORSMode) { + return false; + } + + // For non-blob URIs, compare the URIs. + bool equals = false; + nsresult rv = mURI->Equals(aOther.mURI, &equals); + return NS_SUCCEEDED(rv) && equals; +} + +void ImageCacheKey::EnsureHash() const { + MOZ_ASSERT(mHash.isNothing()); + PLDHashNumber hash = 0; + + // Since we frequently call Hash() several times in a row on the same + // ImageCacheKey, as an optimization we compute our hash once and store it. + + nsPrintfCString ptr("%p", mControlledDocument); + nsAutoCString suffix; + mOriginAttributes.CreateSuffix(suffix); + + nsAutoCString spec; + Unused << mURI->GetSpec(spec); + hash = HashString(spec); + + hash = AddToHash(hash, HashString(suffix), HashString(mIsolationKey), + HashString(ptr)); + mHash.emplace(hash); +} + +/* static */ +void* ImageCacheKey::GetSpecialCaseDocumentToken(Document* aDocument) { + // Cookie-averse documents can never have storage granted to them. Since they + // may not have inner windows, they would require special handling below, so + // just bail out early here. + if (!aDocument || aDocument->IsCookieAverse()) { + return nullptr; + } + + // For controlled documents, we cast the pointer into a void* to avoid + // dereferencing it (since we only use it for comparisons). + RefPtr swm = ServiceWorkerManager::GetInstance(); + if (swm && aDocument->GetController().isSome()) { + return aDocument; + } + + return nullptr; +} + +/* static */ +nsCString ImageCacheKey::GetIsolationKey(Document* aDocument, nsIURI* aURI) { + if (!aDocument || !aDocument->GetInnerWindow()) { + return ""_ns; + } + + // Network-state isolation + if (StaticPrefs::privacy_partition_network_state()) { + OriginAttributes oa; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aDocument, oa); + + nsAutoCString suffix; + oa.CreateSuffix(suffix); + + return std::move(suffix); + } + + // If the window is 3rd party resource, let's see if first-party storage + // access is granted for this image. + if (AntiTrackingUtils::IsThirdPartyWindow(aDocument->GetInnerWindow(), + nullptr)) { + uint32_t rejectedReason = 0; + Unused << rejectedReason; + return StorageDisabledByAntiTracking(aDocument, aURI, rejectedReason) + ? aDocument->GetBaseDomain() + : ""_ns; + } + + // Another scenario is if this image is a 3rd party resource loaded by a + // first party context. In this case, we should check if the nsIChannel has + // been marked as tracking resource, but we don't have the channel yet at + // this point. The best approach here is to be conservative: if we are sure + // that the permission is granted, let's return 0. Otherwise, let's make a + // unique image cache per the top-level document eTLD+1. + if (!ApproximateAllowAccessForWithoutChannel(aDocument->GetInnerWindow(), + aURI)) { + // If we are here, the image is a 3rd-party resource loaded by a first-party + // context. We can just use the document's base domain as the key because it + // should be the same as the top-level document's base domain. + return aDocument + ->GetBaseDomain(); // because we don't have anything better! + } + + return ""_ns; +} + +} // namespace image +} // namespace mozilla diff --git a/image/ImageCacheKey.h b/image/ImageCacheKey.h new file mode 100644 index 0000000000..2be4493081 --- /dev/null +++ b/image/ImageCacheKey.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * ImageCacheKey is the key type for the image cache (see imgLoader.h). + */ + +#ifndef mozilla_image_src_ImageCacheKey_h +#define mozilla_image_src_ImageCacheKey_h + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "PLDHashTable.h" + +class nsIURI; + +namespace mozilla { + +enum CORSMode : uint8_t; + +namespace image { + +/** + * An ImageLib cache entry key. + * + * We key the cache on the initial URI (before any redirects), with some + * canonicalization applied. See ComputeHash() for the details. + * Controlled documents do not share their cache entries with + * non-controlled documents, or other controlled documents. + */ +class ImageCacheKey final { + public: + ImageCacheKey(nsIURI*, CORSMode, const OriginAttributes&, dom::Document*); + + ImageCacheKey(const ImageCacheKey& aOther); + ImageCacheKey(ImageCacheKey&& aOther); + + bool operator==(const ImageCacheKey& aOther) const; + PLDHashNumber Hash() const { + if (MOZ_UNLIKELY(mHash.isNothing())) { + EnsureHash(); + } + return mHash.value(); + } + + /// A weak pointer to the URI. + nsIURI* URI() const { return mURI; } + + CORSMode GetCORSMode() const { return mCORSMode; } + + const OriginAttributes& OriginAttributesRef() const { + return mOriginAttributes; + } + + const nsCString& IsolationKeyRef() const { return mIsolationKey; } + + /// A token indicating which service worker controlled document this entry + /// belongs to, if any. + void* ControlledDocument() const { return mControlledDocument; } + + private: + // For ServiceWorker we need to use the document as + // token for the key. All those exceptions are handled by this method. + static void* GetSpecialCaseDocumentToken(dom::Document* aDocument); + + // For anti-tracking we need to use an isolation key. It can be the suffix of + // the PatitionedPrincipal (see StoragePrincipalHelper.h) or the top-level + // document's base domain. This is handled by this method. + static nsCString GetIsolationKey(dom::Document* aDocument, nsIURI* aURI); + + void EnsureHash() const; + + nsCOMPtr mURI; + const OriginAttributes mOriginAttributes; + void* mControlledDocument; + nsCString mIsolationKey; + mutable Maybe mHash; + const CORSMode mCORSMode; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_src_ImageCacheKey_h diff --git a/image/ImageFactory.cpp b/image/ImageFactory.cpp new file mode 100644 index 0000000000..27541634dc --- /dev/null +++ b/image/ImageFactory.cpp @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageFactory.h" + +#include + +#include "mozilla/Likely.h" + +#include "nsIChannel.h" +#include "nsIFileChannel.h" +#include "nsIObserverService.h" +#include "nsIFile.h" +#include "nsMimeTypes.h" +#include "nsIRequest.h" + +#include "MultipartImage.h" +#include "RasterImage.h" +#include "VectorImage.h" +#include "Image.h" +#include "nsMediaFragmentURIParser.h" +#include "nsContentUtils.h" + +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/ProfilerMarkers.h" + +namespace mozilla { +namespace image { + +/*static*/ +void ImageFactory::Initialize() {} + +static uint32_t ComputeImageFlags(nsIURI* uri, const nsCString& aMimeType, + bool isMultiPart) { + // We default to the static globals. + bool isDiscardable = StaticPrefs::image_mem_discardable(); + bool doDecodeImmediately = StaticPrefs::image_decode_immediately_enabled(); + + // We want UI to be as snappy as possible and not to flicker. Disable + // discarding for chrome URLS. + if (uri->SchemeIs("chrome")) { + isDiscardable = false; + } + + // We don't want resources like the "loading" icon to be discardable either. + if (uri->SchemeIs("resource")) { + isDiscardable = false; + } + + // For multipart/x-mixed-replace, we basically want a direct channel to the + // decoder. Disable everything for this case. + if (isMultiPart) { + isDiscardable = false; + } + + // We have all the information we need. + uint32_t imageFlags = Image::INIT_FLAG_NONE; + if (isDiscardable) { + imageFlags |= Image::INIT_FLAG_DISCARDABLE; + } + if (doDecodeImmediately) { + imageFlags |= Image::INIT_FLAG_DECODE_IMMEDIATELY; + } + if (isMultiPart) { + imageFlags |= Image::INIT_FLAG_TRANSIENT; + } + + // Synchronously decode metadata (including size) if we have a data URI since + // the data is immediately available. + if (uri->SchemeIs("data")) { + imageFlags |= Image::INIT_FLAG_SYNC_LOAD; + } + + return imageFlags; +} + +#ifdef DEBUG +static void NotifyImageLoading(nsIURI* aURI) { + if (!NS_IsMainThread()) { + nsCOMPtr uri(aURI); + nsCOMPtr ev = NS_NewRunnableFunction( + "NotifyImageLoading", [uri]() -> void { NotifyImageLoading(uri); }); + SchedulerGroup::Dispatch(TaskCategory::Other, ev.forget()); + return; + } + + nsCOMPtr obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Can't get an observer service handle"); + if (obs) { + nsAutoCString spec; + aURI->GetSpec(spec); + obs->NotifyObservers(nullptr, "image-loading", + NS_ConvertUTF8toUTF16(spec).get()); + } +} +#endif + +/* static */ +already_AddRefed ImageFactory::CreateImage( + nsIRequest* aRequest, ProgressTracker* aProgressTracker, + const nsCString& aMimeType, nsIURI* aURI, bool aIsMultiPart, + uint64_t aInnerWindowId) { + // Compute the image's initialization flags. + uint32_t imageFlags = ComputeImageFlags(aURI, aMimeType, aIsMultiPart); + +#ifdef DEBUG + // Record the image load for startup performance testing. + if (aURI->SchemeIs("resource") || aURI->SchemeIs("chrome")) { + NotifyImageLoading(aURI); + } +#endif + + if (profiler_thread_is_being_profiled_for_markers()) { + static const size_t sMaxTruncatedLength = 1024; + PROFILER_MARKER_TEXT( + "Image Load", GRAPHICS, MarkerInnerWindowId(aInnerWindowId), + nsContentUtils::TruncatedURLForDisplay(aURI, sMaxTruncatedLength)); + } + + // Select the type of image to create based on MIME type. + if (aMimeType.EqualsLiteral(IMAGE_SVG_XML)) { + return CreateVectorImage(aRequest, aProgressTracker, aMimeType, aURI, + imageFlags, aInnerWindowId); + } else { + return CreateRasterImage(aRequest, aProgressTracker, aMimeType, aURI, + imageFlags, aInnerWindowId); + } +} + +// Marks an image as having an error before returning it. +template +static already_AddRefed BadImage(const char* aMessage, + RefPtr& aImage) { + aImage->SetHasError(); + return aImage.forget(); +} + +/* static */ +already_AddRefed ImageFactory::CreateAnonymousImage( + const nsCString& aMimeType, uint32_t aSizeHint /* = 0 */) { + nsresult rv; + + RefPtr newImage = new RasterImage(); + + RefPtr newTracker = new ProgressTracker(); + newTracker->SetImage(newImage); + newImage->SetProgressTracker(newTracker); + + rv = newImage->Init(aMimeType.get(), Image::INIT_FLAG_SYNC_LOAD); + if (NS_FAILED(rv)) { + return BadImage("RasterImage::Init failed", newImage); + } + + rv = newImage->SetSourceSizeHint(aSizeHint); + if (NS_FAILED(rv)) { + return BadImage("RasterImage::SetSourceSizeHint failed", newImage); + } + + return newImage.forget(); +} + +/* static */ +already_AddRefed ImageFactory::CreateMultipartImage( + Image* aFirstPart, ProgressTracker* aProgressTracker) { + MOZ_ASSERT(aFirstPart); + MOZ_ASSERT(aProgressTracker); + + RefPtr newImage = new MultipartImage(aFirstPart); + aProgressTracker->SetImage(newImage); + newImage->SetProgressTracker(aProgressTracker); + + newImage->Init(); + + return newImage.forget(); +} + +int32_t SaturateToInt32(int64_t val) { + if (val > INT_MAX) { + return INT_MAX; + } + if (val < INT_MIN) { + return INT_MIN; + } + + return static_cast(val); +} + +uint32_t GetContentSize(nsIRequest* aRequest) { + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (channel) { + int64_t size; + nsresult rv = channel->GetContentLength(&size); + if (NS_SUCCEEDED(rv)) { + return std::max(SaturateToInt32(size), 0); + } + } + + // Use the file size as a size hint for file channels. + nsCOMPtr fileChannel(do_QueryInterface(aRequest)); + if (fileChannel) { + nsCOMPtr file; + nsresult rv = fileChannel->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + int64_t filesize; + rv = file->GetFileSize(&filesize); + if (NS_SUCCEEDED(rv)) { + return std::max(SaturateToInt32(filesize), 0); + } + } + } + + // Fallback - neither http nor file. We'll use dynamic allocation. + return 0; +} + +/* static */ +already_AddRefed ImageFactory::CreateRasterImage( + nsIRequest* aRequest, ProgressTracker* aProgressTracker, + const nsCString& aMimeType, nsIURI* aURI, uint32_t aImageFlags, + uint64_t aInnerWindowId) { + MOZ_ASSERT(aProgressTracker); + + nsresult rv; + + RefPtr newImage = new RasterImage(aURI); + aProgressTracker->SetImage(newImage); + newImage->SetProgressTracker(aProgressTracker); + + rv = newImage->Init(aMimeType.get(), aImageFlags); + if (NS_FAILED(rv)) { + return BadImage("RasterImage::Init failed", newImage); + } + + newImage->SetInnerWindowID(aInnerWindowId); + + rv = newImage->SetSourceSizeHint(GetContentSize(aRequest)); + if (NS_FAILED(rv)) { + return BadImage("RasterImage::SetSourceSizeHint failed", newImage); + } + + return newImage.forget(); +} + +/* static */ +already_AddRefed ImageFactory::CreateVectorImage( + nsIRequest* aRequest, ProgressTracker* aProgressTracker, + const nsCString& aMimeType, nsIURI* aURI, uint32_t aImageFlags, + uint64_t aInnerWindowId) { + MOZ_ASSERT(aProgressTracker); + + nsresult rv; + + RefPtr newImage = new VectorImage(aURI); + aProgressTracker->SetImage(newImage); + newImage->SetProgressTracker(aProgressTracker); + + rv = newImage->Init(aMimeType.get(), aImageFlags); + if (NS_FAILED(rv)) { + return BadImage("VectorImage::Init failed", newImage); + } + + newImage->SetInnerWindowID(aInnerWindowId); + + rv = newImage->OnStartRequest(aRequest); + if (NS_FAILED(rv)) { + return BadImage("VectorImage::OnStartRequest failed", newImage); + } + + return newImage.forget(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/ImageFactory.h b/image/ImageFactory.h new file mode 100644 index 0000000000..1f9b677739 --- /dev/null +++ b/image/ImageFactory.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageFactory_h +#define mozilla_image_ImageFactory_h + +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "nsStringFwd.h" + +class nsIRequest; +class nsIURI; + +namespace mozilla { +namespace image { + +class Image; +class MultipartImage; +class ProgressTracker; + +class ImageFactory { + public: + /** + * Registers vars with Preferences. Should only be called on the main thread. + */ + static void Initialize(); + + /** + * Creates a new image with the given properties. + * Can be called on or off the main thread. + * + * @param aRequest The associated request. + * @param aProgressTracker A status tracker for the image to use. + * @param aMimeType The mimetype of the image. + * @param aURI The URI of the image. + * @param aIsMultiPart Whether the image is part of a multipart request. + * @param aInnerWindowId The window this image belongs to. + */ + static already_AddRefed CreateImage(nsIRequest* aRequest, + ProgressTracker* aProgressTracker, + const nsCString& aMimeType, + nsIURI* aURI, bool aIsMultiPart, + uint64_t aInnerWindowId); + /** + * Creates a new image which isn't associated with a URI or loaded through + * the usual image loading mechanism. + * + * @param aMimeType The mimetype of the image. + * @param aSizeHint The length of the source data for the image. + */ + static already_AddRefed CreateAnonymousImage( + const nsCString& aMimeType, uint32_t aSizeHint = 0); + + /** + * Creates a new multipart/x-mixed-replace image wrapper, and initializes it + * with the first part. Subsequent parts should be passed to the existing + * MultipartImage via MultipartImage::BeginTransitionToPart(). + * + * @param aFirstPart An image containing the first part of the multipart + * stream. + * @param aProgressTracker A progress tracker for the multipart image. + */ + static already_AddRefed CreateMultipartImage( + Image* aFirstPart, ProgressTracker* aProgressTracker); + + private: + // Factory functions that create specific types of image containers. + static already_AddRefed CreateRasterImage( + nsIRequest* aRequest, ProgressTracker* aProgressTracker, + const nsCString& aMimeType, nsIURI* aURI, uint32_t aImageFlags, + uint64_t aInnerWindowId); + + static already_AddRefed CreateVectorImage( + nsIRequest* aRequest, ProgressTracker* aProgressTracker, + const nsCString& aMimeType, nsIURI* aURI, uint32_t aImageFlags, + uint64_t aInnerWindowId); + + // This is a static factory class, so disallow instantiation. + virtual ~ImageFactory() = 0; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageFactory_h diff --git a/image/ImageLogging.h b/image/ImageLogging.h new file mode 100644 index 0000000000..c36f4e8dbd --- /dev/null +++ b/image/ImageLogging.h @@ -0,0 +1,185 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageLogging_h +#define mozilla_image_ImageLogging_h + +#include "mozilla/Logging.h" +#include "Image.h" +#include "nsIURI.h" +#include "prinrval.h" + +static mozilla::LazyLogModule gImgLog("imgRequest"); + +#define GIVE_ME_MS_NOW() PR_IntervalToMilliseconds(PR_IntervalNow()) + +using mozilla::LogLevel; + +class LogScope { + public: + LogScope(mozilla::LogModule* aLog, void* aFrom, const char* aFunc) + : mLog(aLog), mFrom(aFrom), mFunc(aFunc) { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s {ENTER}\n", GIVE_ME_MS_NOW(), mFrom, mFunc)); + } + + /* const char * constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, const char* paramValue) + : mLog(aLog), mFrom(from), mFunc(fn) { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%s\") {ENTER}\n", GIVE_ME_MS_NOW(), mFrom, + mFunc, paramName, paramValue)); + } + + /* void ptr constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, const void* paramValue) + : mLog(aLog), mFrom(from), mFunc(fn) { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=%p) {ENTER}\n", GIVE_ME_MS_NOW(), mFrom, + mFunc, paramName, paramValue)); + } + + /* int32_t constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, int32_t paramValue) + : mLog(aLog), mFrom(from), mFunc(fn) { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%d\") {ENTER}\n", GIVE_ME_MS_NOW(), mFrom, + mFunc, paramName, paramValue)); + } + + /* uint32_t constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, uint32_t paramValue) + : mLog(aLog), mFrom(from), mFunc(fn) { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%d\") {ENTER}\n", GIVE_ME_MS_NOW(), mFrom, + mFunc, paramName, paramValue)); + } + + /* nsIURI constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, nsIURI* aURI) + : mLog(aLog), mFrom(from), mFunc(fn) { + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + static const size_t sMaxTruncatedLength = 1024; + nsAutoCString spec(""); + if (aURI) { + aURI->GetSpec(spec); + if (spec.Length() >= sMaxTruncatedLength) { + spec.Truncate(sMaxTruncatedLength); + } + } + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%s\") {ENTER}\n", GIVE_ME_MS_NOW(), from, + fn, paramName, spec.get())); + } + } + + /* Image constructor */ + LogScope(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, mozilla::image::Image* aImage) + : LogScope(aLog, from, fn, paramName, + aImage ? aImage->GetURI() : nullptr) {} + + ~LogScope() { + MOZ_LOG(mLog, LogLevel::Debug, + ("%d [this=%p] %s {EXIT}\n", GIVE_ME_MS_NOW(), mFrom, mFunc)); + } + + private: + mozilla::LogModule* mLog; + void* mFrom; + const char* mFunc; +}; + +class LogFunc { + public: + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn) { + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s\n", GIVE_ME_MS_NOW(), from, fn)); + } + + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, const char* paramValue) { + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%s\")\n", GIVE_ME_MS_NOW(), from, fn, + paramName, paramValue)); + } + + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, const void* paramValue) { + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%p\")\n", GIVE_ME_MS_NOW(), from, fn, + paramName, paramValue)); + } + + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, uint32_t paramValue) { + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%d\")\n", GIVE_ME_MS_NOW(), from, fn, + paramName, paramValue)); + } + + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, nsIURI* aURI) { + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + static const size_t sMaxTruncatedLength = 1024; + nsAutoCString spec(""); + if (aURI) { + aURI->GetSpec(spec); + if (spec.Length() >= sMaxTruncatedLength) { + spec.Truncate(sMaxTruncatedLength); + } + } + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s (%s=\"%s\")\n", GIVE_ME_MS_NOW(), from, fn, + paramName, spec.get())); + } + } + + LogFunc(mozilla::LogModule* aLog, void* from, const char* fn, + const char* paramName, mozilla::image::Image* aImage) + : LogFunc(aLog, from, fn, paramName, + aImage ? aImage->GetURI() : nullptr) {} +}; + +class LogMessage { + public: + LogMessage(mozilla::LogModule* aLog, void* from, const char* fn, + const char* msg) { + MOZ_LOG(aLog, LogLevel::Debug, + ("%d [this=%p] %s -- %s\n", GIVE_ME_MS_NOW(), from, fn, msg)); + } +}; + +#define LOG_SCOPE_APPEND_LINE_NUMBER_PASTE(id, line) id##line +#define LOG_SCOPE_APPEND_LINE_NUMBER_EXPAND(id, line) \ + LOG_SCOPE_APPEND_LINE_NUMBER_PASTE(id, line) +#define LOG_SCOPE_APPEND_LINE_NUMBER(id) \ + LOG_SCOPE_APPEND_LINE_NUMBER_EXPAND(id, __LINE__) + +#define LOG_SCOPE(l, s) \ + LogScope LOG_SCOPE_APPEND_LINE_NUMBER(LOG_SCOPE_TMP_VAR)(l, this, s) + +#define LOG_SCOPE_WITH_PARAM(l, s, pn, pv) \ + LogScope LOG_SCOPE_APPEND_LINE_NUMBER(LOG_SCOPE_TMP_VAR)(l, this, s, pn, pv) + +#define LOG_FUNC(l, s) LogFunc(l, this, s) + +#define LOG_FUNC_WITH_PARAM(l, s, pn, pv) LogFunc(l, this, s, pn, pv) + +#define LOG_STATIC_FUNC(l, s) LogFunc(l, nullptr, s) + +#define LOG_STATIC_FUNC_WITH_PARAM(l, s, pn, pv) LogFunc(l, nullptr, s, pn, pv) + +#define LOG_MSG(l, s, m) LogMessage(l, this, s, m) + +#define LOG_MSG_WITH_PARAM LOG_FUNC_WITH_PARAM + +#endif // mozilla_image_ImageLogging_h diff --git a/image/ImageMemoryReporter.cpp b/image/ImageMemoryReporter.cpp new file mode 100644 index 0000000000..0afb890d00 --- /dev/null +++ b/image/ImageMemoryReporter.cpp @@ -0,0 +1,185 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageMemoryReporter.h" +#include "Image.h" +#include "base/process_util.h" +#include "mozilla/layers/SharedSurfacesParent.h" +#include "mozilla/StaticPrefs_image.h" +#include "nsIMemoryReporter.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace image { + +ImageMemoryReporter::WebRenderReporter* ImageMemoryReporter::sWrReporter; + +class ImageMemoryReporter::WebRenderReporter final : public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + + WebRenderReporter() {} + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override { + layers::SharedSurfacesMemoryReport report; + layers::SharedSurfacesParent::AccumulateMemoryReport(report); + ReportSharedSurfaces(aHandleReport, aData, /* aIsForCompositor */ true, + report); + return NS_OK; + } + + private: + virtual ~WebRenderReporter() {} +}; + +NS_IMPL_ISUPPORTS(ImageMemoryReporter::WebRenderReporter, nsIMemoryReporter) + +/* static */ +void ImageMemoryReporter::InitForWebRender() { + MOZ_ASSERT(XRE_IsParentProcess() || XRE_IsGPUProcess()); + if (!sWrReporter) { + sWrReporter = new WebRenderReporter(); + RegisterStrongMemoryReporter(sWrReporter); + } +} + +/* static */ +void ImageMemoryReporter::ShutdownForWebRender() { + MOZ_ASSERT(XRE_IsParentProcess() || XRE_IsGPUProcess()); + if (sWrReporter) { + UnregisterStrongMemoryReporter(sWrReporter); + sWrReporter = nullptr; + } +} + +/* static */ +void ImageMemoryReporter::ReportSharedSurfaces( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + const layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + ReportSharedSurfaces(aHandleReport, aData, + /* aIsForCompositor */ false, aSharedSurfaces); +} + +/* static */ +void ImageMemoryReporter::ReportSharedSurfaces( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aIsForCompositor, + const layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + MOZ_ASSERT_IF(aIsForCompositor, XRE_IsParentProcess() || XRE_IsGPUProcess()); + MOZ_ASSERT_IF(!aIsForCompositor, + XRE_IsParentProcess() || XRE_IsContentProcess()); + + for (auto i = aSharedSurfaces.mSurfaces.begin(); + i != aSharedSurfaces.mSurfaces.end(); ++i) { + ReportSharedSurface(aHandleReport, aData, aIsForCompositor, i->first, + i->second); + } +} + +/* static */ +void ImageMemoryReporter::ReportSharedSurface( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aIsForCompositor, uint64_t aExternalId, + const layers::SharedSurfacesMemoryReport::SurfaceEntry& aEntry) { + nsAutoCString path; + if (aIsForCompositor) { + path.AppendLiteral("gfx/webrender/images/mapped_from_owner/"); + } else { + path.AppendLiteral("gfx/webrender/images/owner_cache_missing/"); + } + + if (aIsForCompositor) { + path.AppendLiteral("pid="); + path.AppendInt(uint32_t(aEntry.mCreatorPid)); + path.AppendLiteral("/"); + } + + if (StaticPrefs::image_mem_debug_reporting()) { + path.AppendInt(aExternalId, 16); + path.AppendLiteral("/"); + } + + path.AppendLiteral("image("); + path.AppendInt(aEntry.mSize.width); + path.AppendLiteral("x"); + path.AppendInt(aEntry.mSize.height); + path.AppendLiteral(", compositor_ref:"); + path.AppendInt(aEntry.mConsumers); + path.AppendLiteral(", creator_ref:"); + path.AppendInt(aEntry.mCreatorRef); + path.AppendLiteral(")/decoded-"); + + size_t surfaceSize = mozilla::ipc::SharedMemory::PageAlignedSize( + aEntry.mSize.height * aEntry.mStride); + + // If this memory has already been reported elsewhere (e.g. as part of our + // explicit section in the surface cache), we don't want report it again as + // KIND_NONHEAP and have it counted again. The paths must be different if the + // kinds are different to avoid problems when diffing memory reports. + bool sameProcess = aEntry.mCreatorPid == base::GetCurrentProcId(); + int32_t kind; + if (aIsForCompositor && !sameProcess) { + path.AppendLiteral("nonheap"); + kind = nsIMemoryReporter::KIND_NONHEAP; + } else { + path.AppendLiteral("other"); + kind = nsIMemoryReporter::KIND_OTHER; + } + + constexpr auto desc = "Decoded image data stored in shared memory."_ns; + aHandleReport->Callback(""_ns, path, kind, nsIMemoryReporter::UNITS_BYTES, + surfaceSize, desc, aData); +} + +/* static */ +void ImageMemoryReporter::AppendSharedSurfacePrefix( + nsACString& aPathPrefix, const SurfaceMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + uint64_t extId = aCounter.Values().ExternalId(); + if (extId) { + auto gpuEntry = aSharedSurfaces.mSurfaces.find(extId); + + if (StaticPrefs::image_mem_debug_reporting()) { + aPathPrefix.AppendLiteral(", external_id:"); + aPathPrefix.AppendInt(extId, 16); + if (gpuEntry != aSharedSurfaces.mSurfaces.end()) { + aPathPrefix.AppendLiteral(", compositor_ref:"); + aPathPrefix.AppendInt(gpuEntry->second.mConsumers); + } else { + aPathPrefix.AppendLiteral(", compositor_ref:missing"); + } + } + + if (gpuEntry != aSharedSurfaces.mSurfaces.end()) { + MOZ_ASSERT(gpuEntry->second.mCreatorRef); + aSharedSurfaces.mSurfaces.erase(gpuEntry); + } + } +} + +/* static */ +void ImageMemoryReporter::TrimSharedSurfaces( + const ImageMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + if (aSharedSurfaces.mSurfaces.empty()) { + return; + } + + for (const SurfaceMemoryCounter& counter : aCounter.Surfaces()) { + uint64_t extId = counter.Values().ExternalId(); + if (extId) { + auto gpuEntry = aSharedSurfaces.mSurfaces.find(extId); + if (gpuEntry != aSharedSurfaces.mSurfaces.end()) { + MOZ_ASSERT(gpuEntry->second.mCreatorRef); + aSharedSurfaces.mSurfaces.erase(gpuEntry); + } + } + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/ImageMemoryReporter.h b/image/ImageMemoryReporter.h new file mode 100644 index 0000000000..ab7d55c46d --- /dev/null +++ b/image/ImageMemoryReporter.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageMemoryReporter_h +#define mozilla_image_ImageMemoryReporter_h + +#include +#include "nsString.h" +#include "mozilla/layers/SharedSurfacesMemoryReport.h" + +class nsISupports; +class nsIHandleReportCallback; + +namespace mozilla { +namespace image { +struct ImageMemoryCounter; +struct SurfaceMemoryCounter; + +class ImageMemoryReporter final { + public: + /** + * Initializes image related memory reporting in the compositor process when + * using WebRender. + */ + static void InitForWebRender(); + + /** + * Tears down image related memory reporting in the compositor process when + * using WebRender. + */ + static void ShutdownForWebRender(); + + /** + * Report all remaining entries in the shared surface's memory report. This + * should be used by the content or main process to allow reporting any + * entries that is was unable to cross reference with the local surface cache. + * These are candidates for having been leaked. This should be used in + * conjunction with AppendSharedSurfacePrefix and/or TrimSharedSurfaces to + * produce the expected result. + */ + static void ReportSharedSurfaces( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + const layers::SharedSurfacesMemoryReport& aSharedSurfaces); + + /** + * Adjust the path prefix for a surface to include any additional metadata for + * the shared surface, if any. It will also remove any corresponding entries + * in the given memory report. + */ + static void AppendSharedSurfacePrefix( + nsACString& aPathPrefix, const SurfaceMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces); + + /** + * Remove all entries in the memory report for the given set of surfaces for + * an image. This is useful when we aren't reporting on a particular image + * because it isn't notable. + */ + static void TrimSharedSurfaces( + const ImageMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces); + + private: + /** + * Report all remaining entries in the shared surface's memory report. + * + * aIsForCompositor controls how to interpret what remains in the report. If + * true, this should mirror exactly what is currently in + * SharedSurfacesParent's cache. This will report entries that are currently + * mapped into the compositor process. If false, then we are in a content or + * main process, and it should have removed entries that also exist in its + * local surface cache -- thus any remaining entries are those that are + * candidates for leaks. + */ + static void ReportSharedSurfaces( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aIsForCompositor, + const layers::SharedSurfacesMemoryReport& aSharedSurfaces); + + static void ReportSharedSurface( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aIsForCompositor, uint64_t aExternalId, + const layers::SharedSurfacesMemoryReport::SurfaceEntry& aEntry); + + class WebRenderReporter; + static WebRenderReporter* sWrReporter; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageMemoryReporter_h diff --git a/image/ImageMetadata.h b/image/ImageMetadata.h new file mode 100644 index 0000000000..0648812339 --- /dev/null +++ b/image/ImageMetadata.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageMetadata_h +#define mozilla_image_ImageMetadata_h + +#include +#include +#include "FrameTimeout.h" +#include "Orientation.h" +#include "mozilla/Maybe.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/gfx/Rect.h" +#include "mozilla/image/Resolution.h" +#include "nsSize.h" +#include "nsTArray.h" + +namespace mozilla::image { + +// The metadata about an image that decoders accumulate as they decode. +class ImageMetadata { + public: + ImageMetadata() = default; + + void SetHotspot(uint16_t aHotspotX, uint16_t aHotspotY) { + mHotspot = Some(gfx::IntPoint(aHotspotX, aHotspotY)); + } + gfx::IntPoint GetHotspot() const { return *mHotspot; } + bool HasHotspot() const { return mHotspot.isSome(); } + + void SetLoopCount(int32_t loopcount) { mLoopCount = loopcount; } + int32_t GetLoopCount() const { return mLoopCount; } + + void SetLoopLength(FrameTimeout aLength) { mLoopLength = Some(aLength); } + FrameTimeout GetLoopLength() const { return *mLoopLength; } + bool HasLoopLength() const { return mLoopLength.isSome(); } + + void SetFirstFrameTimeout(FrameTimeout aTimeout) { + mFirstFrameTimeout = aTimeout; + } + FrameTimeout GetFirstFrameTimeout() const { return mFirstFrameTimeout; } + + void SetFirstFrameRefreshArea(const gfx::IntRect& aRefreshArea) { + mFirstFrameRefreshArea = Some(aRefreshArea); + } + gfx::IntRect GetFirstFrameRefreshArea() const { + return *mFirstFrameRefreshArea; + } + bool HasFirstFrameRefreshArea() const { + return mFirstFrameRefreshArea.isSome(); + } + + void SetSize(int32_t aWidth, int32_t aHeight, Orientation aOrientation, + Resolution aResolution) { + if (!HasSize()) { + mSize.emplace( + aOrientation.ToOriented(UnorientedIntSize(aWidth, aHeight))); + mOrientation.emplace(aOrientation); + mResolution = aResolution; + } + } + OrientedIntSize GetSize() const { return *mSize; } + bool HasSize() const { return mSize.isSome(); } + + void AddNativeSize(const OrientedIntSize& aSize) { + mNativeSizes.AppendElement(aSize); + } + + Resolution GetResolution() const { return mResolution; } + + const nsTArray& GetNativeSizes() const { + return mNativeSizes; + } + + Orientation GetOrientation() const { return *mOrientation; } + bool HasOrientation() const { return mOrientation.isSome(); } + + void SetHasAnimation() { mHasAnimation = true; } + bool HasAnimation() const { return mHasAnimation; } + + private: + /// The hotspot found on cursors, if present. + Maybe mHotspot; + + /// The loop count for animated images, or -1 for infinite loop. + int32_t mLoopCount = -1; + + /// The resolution of the image in dppx. + Resolution mResolution; + + // The total length of a single loop through an animated image. + Maybe mLoopLength; + + /// The timeout of an animated image's first frame. + FrameTimeout mFirstFrameTimeout = FrameTimeout::Forever(); + + // The area of the image that needs to be invalidated when the animation + // loops. + Maybe mFirstFrameRefreshArea; + + Maybe mSize; + Maybe mOrientation; + + // Sizes the image can natively decode to. + CopyableTArray mNativeSizes; + + bool mHasAnimation = false; +}; + +} // namespace mozilla::image + +#endif // mozilla_image_ImageMetadata_h diff --git a/image/ImageOps.cpp b/image/ImageOps.cpp new file mode 100644 index 0000000000..875dc7b985 --- /dev/null +++ b/image/ImageOps.cpp @@ -0,0 +1,248 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageOps.h" + +#include "ClippedImage.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "DynamicImage.h" +#include "FrozenImage.h" +#include "IDecodingTask.h" +#include "Image.h" +#include "ImageMetadata.h" +#include "imgIContainer.h" +#include "mozilla/gfx/2D.h" +#include "nsNetUtil.h" // for NS_NewBufferedInputStream +#include "nsStreamUtils.h" +#include "OrientedImage.h" +#include "SourceBuffer.h" + +using namespace mozilla::gfx; + +namespace mozilla::image { + +/* static */ +already_AddRefed ImageOps::Freeze(Image* aImage) { + RefPtr frozenImage = new FrozenImage(aImage); + return frozenImage.forget(); +} + +/* static */ +already_AddRefed ImageOps::Freeze(imgIContainer* aImage) { + nsCOMPtr frozenImage = + new FrozenImage(static_cast(aImage)); + return frozenImage.forget(); +} + +/* static */ +already_AddRefed ImageOps::Clip(Image* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize) { + RefPtr clippedImage = + new ClippedImage(aImage, aClip, aSVGViewportSize); + return clippedImage.forget(); +} + +/* static */ +already_AddRefed ImageOps::Clip( + imgIContainer* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize) { + nsCOMPtr clippedImage = + new ClippedImage(static_cast(aImage), aClip, aSVGViewportSize); + return clippedImage.forget(); +} + +/* static */ +already_AddRefed ImageOps::Orient(Image* aImage, + Orientation aOrientation) { + if (aOrientation.IsIdentity()) { + return do_AddRef(aImage); + } + RefPtr image = new OrientedImage(aImage, aOrientation); + return image.forget(); +} + +/* static */ +already_AddRefed ImageOps::Orient(imgIContainer* aImage, + Orientation aOrientation) { + return Orient(static_cast(aImage), aOrientation); +} + +/* static */ +already_AddRefed ImageOps::Unorient(imgIContainer* aImage) { + return Orient(aImage, aImage->GetOrientation().Reversed()); +} + +/* static */ +already_AddRefed ImageOps::CreateFromDrawable( + gfxDrawable* aDrawable) { + nsCOMPtr drawableImage = new DynamicImage(aDrawable); + return drawableImage.forget(); +} + +class ImageOps::ImageBufferImpl final : public ImageOps::ImageBuffer { + public: + explicit ImageBufferImpl(already_AddRefed aSourceBuffer) + : mSourceBuffer(aSourceBuffer) {} + + protected: + ~ImageBufferImpl() override {} + + already_AddRefed GetSourceBuffer() const override { + RefPtr sourceBuffer = mSourceBuffer; + return sourceBuffer.forget(); + } + + private: + RefPtr mSourceBuffer; +}; + +/* static */ already_AddRefed +ImageOps::CreateImageBuffer(already_AddRefed aInputStream) { + nsCOMPtr inputStream = std::move(aInputStream); + MOZ_ASSERT(inputStream); + + nsresult rv; + + // Prepare the input stream. + if (!NS_InputStreamIsBuffered(inputStream)) { + nsCOMPtr bufStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream), + inputStream.forget(), 1024); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + inputStream = std::move(bufStream); + } + + // Figure out how much data we've been passed. + uint64_t length; + rv = inputStream->Available(&length); + if (NS_FAILED(rv) || length > UINT32_MAX) { + return nullptr; + } + + // Write the data into a SourceBuffer. + RefPtr sourceBuffer = new SourceBuffer(); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + if (NS_FAILED(rv)) { + return nullptr; + } + // Make sure our sourceBuffer is marked as complete. + if (sourceBuffer->IsComplete()) { + NS_WARNING( + "The SourceBuffer was unexpectedly marked as complete. This may " + "indicate either an OOM condition, or that imagelib was not " + "initialized properly."); + return nullptr; + } + sourceBuffer->Complete(NS_OK); + + RefPtr imageBuffer = new ImageBufferImpl(sourceBuffer.forget()); + return imageBuffer.forget(); +} + +/* static */ +nsresult ImageOps::DecodeMetadata(already_AddRefed aInputStream, + const nsACString& aMimeType, + ImageMetadata& aMetadata) { + nsCOMPtr inputStream = std::move(aInputStream); + RefPtr buffer = CreateImageBuffer(inputStream.forget()); + return DecodeMetadata(buffer, aMimeType, aMetadata); +} + +/* static */ +nsresult ImageOps::DecodeMetadata(ImageBuffer* aBuffer, + const nsACString& aMimeType, + ImageMetadata& aMetadata) { + if (!aBuffer) { + return NS_ERROR_FAILURE; + } + + RefPtr sourceBuffer = aBuffer->GetSourceBuffer(); + if (NS_WARN_IF(!sourceBuffer)) { + return NS_ERROR_FAILURE; + } + + // Create a decoder. + DecoderType decoderType = + DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get()); + RefPtr decoder = DecoderFactory::CreateAnonymousMetadataDecoder( + decoderType, WrapNotNull(sourceBuffer)); + if (!decoder) { + return NS_ERROR_FAILURE; + } + + // Run the decoder synchronously. + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + task->Run(); + if (!decoder->GetDecodeDone() || decoder->HasError()) { + return NS_ERROR_FAILURE; + } + + aMetadata = decoder->GetImageMetadata(); + if (aMetadata.GetNativeSizes().IsEmpty() && aMetadata.HasSize()) { + aMetadata.AddNativeSize(aMetadata.GetSize()); + } + + return NS_OK; +} + +/* static */ already_AddRefed ImageOps::DecodeToSurface( + already_AddRefed aInputStream, const nsACString& aMimeType, + uint32_t aFlags, const Maybe& aSize /* = Nothing() */) { + nsCOMPtr inputStream = std::move(aInputStream); + RefPtr buffer = CreateImageBuffer(inputStream.forget()); + return DecodeToSurface(buffer, aMimeType, aFlags, aSize); +} + +/* static */ already_AddRefed ImageOps::DecodeToSurface( + ImageBuffer* aBuffer, const nsACString& aMimeType, uint32_t aFlags, + const Maybe& aSize /* = Nothing() */) { + if (!aBuffer) { + return nullptr; + } + + RefPtr sourceBuffer = aBuffer->GetSourceBuffer(); + if (NS_WARN_IF(!sourceBuffer)) { + return nullptr; + } + + // Create a decoder. + DecoderType decoderType = + DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get()); + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, WrapNotNull(sourceBuffer), aSize, + DecoderFlags::FIRST_FRAME_ONLY, ToSurfaceFlags(aFlags)); + if (!decoder) { + return nullptr; + } + + // Run the decoder synchronously. + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + task->Run(); + if (!decoder->GetDecodeDone() || decoder->HasError()) { + return nullptr; + } + + // Pull out the surface. + RawAccessFrameRef frame = decoder->GetCurrentFrameRef(); + if (!frame) { + return nullptr; + } + + RefPtr surface = frame->GetSourceSurface(); + if (!surface) { + return nullptr; + } + + return surface.forget(); +} + +} // namespace mozilla::image diff --git a/image/ImageOps.h b/image/ImageOps.h new file mode 100644 index 0000000000..3a97ad00ba --- /dev/null +++ b/image/ImageOps.h @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageOps_h +#define mozilla_image_ImageOps_h + +#include "nsCOMPtr.h" +#include "nsRect.h" +#include "ImageMetadata.h" + +class gfxDrawable; +class imgIContainer; +class nsIInputStream; + +namespace mozilla { + +namespace gfx { +class SourceSurface; +} + +namespace image { + +class Image; +struct Orientation; +class SourceBuffer; + +class ImageOps { + public: + class ImageBuffer { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageOps::ImageBuffer); + + protected: + friend class ImageOps; + + ImageBuffer() = default; + virtual ~ImageBuffer() = default; + + virtual already_AddRefed GetSourceBuffer() const = 0; + }; + + /** + * Creates a version of an existing image which does not animate and is frozen + * at the first frame. + * + * @param aImage The existing image. + */ + static already_AddRefed Freeze(Image* aImage); + static already_AddRefed Freeze(imgIContainer* aImage); + + /** + * Creates a clipped version of an existing image. Animation is unaffected. + * + * @param aImage The existing image. + * @param aClip The rectangle to clip the image against. + * @param aSVGViewportSize The specific viewort size of aImage. Unless aImage + * is a vector image without intrinsic size, this + * argument should be pass as Nothing(). + */ + static already_AddRefed Clip( + Image* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize = Nothing()); + static already_AddRefed Clip( + imgIContainer* aImage, nsIntRect aClip, + const Maybe& aSVGViewportSize = Nothing()); + + /** + * Creates a version of an existing image which is rotated and/or flipped to + * the specified orientation. + * + * @param aImage The existing image. + * @param aOrientation The desired orientation. + */ + static already_AddRefed Orient(Image* aImage, + Orientation aOrientation); + static already_AddRefed Orient(imgIContainer* aImage, + Orientation aOrientation); + + /** + * Creates a version of an existing image which undoes any rotation and/or + * flipping that it has automatically handled. + * + * This only undoes the effect of a RasterImage's automatic orientation + * handling. + */ + static already_AddRefed Unorient(imgIContainer* aImage); + + /** + * Creates an image from a gfxDrawable. + * + * @param aDrawable The gfxDrawable. + */ + static already_AddRefed CreateFromDrawable( + gfxDrawable* aDrawable); + + /** + * Create a buffer to be used with DecodeMetadata and DecodeToSurface. Reusing + * an ImageBuffer representing the given input stream is more efficient if one + * has multiple Decode* calls to make on that stream. + * + * @param aInputStream An input stream containing an encoded image. The + * ownership is taken. + * @return An image buffer derived from the input stream. + */ + static already_AddRefed CreateImageBuffer( + already_AddRefed aInputStream); + + /** + * Decodes an image's metadata from an nsIInputStream into the given + * structure. This function may be called off-main-thread. + * + * @param aInputStream An input stream containing an encoded image. Ownership + * is taken. + * @param aMimeType The MIME type of the image. + * @param aMetadata Where the image metadata is stored upon success. + * @return The status of the operation. + */ + static nsresult DecodeMetadata(already_AddRefed aInputStream, + const nsACString& aMimeType, + ImageMetadata& aMetadata); + + /** + * Same as above but takes an ImageBuffer instead of nsIInputStream. + */ + static nsresult DecodeMetadata(ImageBuffer* aBuffer, + const nsACString& aMimeType, + ImageMetadata& aMetadata); + + /** + * Decodes an image from an nsIInputStream directly into a SourceSurface, + * without ever creating an Image or imgIContainer (which are mostly + * main-thread-only). That means that this function may be called + * off-main-thread. + * + * @param aInputStream An input stream containing an encoded image. The + * ownership is taken. + * @param aMimeType The MIME type of the image. + * @param aFlags Flags of the imgIContainer::FLAG_DECODE_* variety. + * @return A SourceSurface containing the first frame of the image at its + * intrinsic size, or nullptr if the image cannot be decoded. + */ + static already_AddRefed DecodeToSurface( + already_AddRefed aInputStream, + const nsACString& aMimeType, uint32_t aFlags, + const Maybe& aSize = Nothing()); + + /** + * Same as above but takes an ImageBuffer instead of nsIInputStream. + */ + static already_AddRefed DecodeToSurface( + ImageBuffer* aBuffer, const nsACString& aMimeType, uint32_t aFlags, + const Maybe& aSize = Nothing()); + + private: + class ImageBufferImpl; + + // This is a static utility class, so disallow instantiation. + virtual ~ImageOps() = 0; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageOps_h diff --git a/image/ImageRegion.h b/image/ImageRegion.h new file mode 100644 index 0000000000..ed592bbbea --- /dev/null +++ b/image/ImageRegion.h @@ -0,0 +1,273 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageRegion_h +#define mozilla_image_ImageRegion_h + +#include "gfxMatrix.h" +#include "gfxPoint.h" +#include "gfxRect.h" +#include "gfxTypes.h" +#include "mozilla/gfx/Matrix.h" +#include "mozilla/gfx/Types.h" +#include "nsSize.h" +#include "PLDHashTable.h" // for PLDHashNumber + +namespace mozilla { +namespace image { + +/** + * An axis-aligned rectangle in tiled image space, with an optional sampling + * restriction rect. The drawing code ensures that if a sampling restriction + * rect is present, any pixels sampled during the drawing process are found + * within that rect. + * + * The sampling restriction rect exists primarily for callers which perform + * pixel snapping. Other callers should generally use one of the Create() + * overloads. + */ +class ImageRegion { + typedef mozilla::gfx::ExtendMode ExtendMode; + + public: + static ImageRegion Empty() { + return ImageRegion(gfxRect(), ExtendMode::CLAMP); + } + + static ImageRegion Create(const gfxRect& aRect, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageRegion(aRect, aExtendMode); + } + + static ImageRegion Create(const gfxSize& aSize, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageRegion(gfxRect(0, 0, aSize.width, aSize.height), aExtendMode); + } + + static ImageRegion Create(const nsIntSize& aSize, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageRegion(gfxRect(0, 0, aSize.width, aSize.height), aExtendMode); + } + + static ImageRegion CreateWithSamplingRestriction( + const gfxRect& aRect, const gfxRect& aRestriction, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageRegion(aRect, aRestriction, aExtendMode); + } + + bool IsRestricted() const { return mIsRestricted; } + const gfxRect& Rect() const { return mRect; } + + const gfxRect& Restriction() const { + MOZ_ASSERT(mIsRestricted); + return mRestriction; + } + + bool RestrictionContains(const gfxRect& aRect) const { + if (!mIsRestricted) { + return true; + } + return mRestriction.Contains(aRect); + } + + ImageRegion Intersect(const gfxRect& aRect) const { + if (mIsRestricted) { + return CreateWithSamplingRestriction(aRect.Intersect(mRect), + aRect.Intersect(mRestriction)); + } + return Create(aRect.Intersect(mRect)); + } + + gfxRect IntersectAndRestrict(const gfxRect& aRect) const { + gfxRect intersection = mRect.Intersect(aRect); + if (mIsRestricted) { + intersection = mRestriction.Intersect(intersection); + } + return intersection; + } + + void MoveBy(gfxFloat dx, gfxFloat dy) { + mRect.MoveBy(dx, dy); + if (mIsRestricted) { + mRestriction.MoveBy(dx, dy); + } + } + + void Scale(gfxFloat sx, gfxFloat sy) { + mRect.Scale(sx, sy); + if (mIsRestricted) { + mRestriction.Scale(sx, sy); + } + } + + void TransformBy(const gfxMatrix& aMatrix) { + mRect = aMatrix.TransformRect(mRect); + if (mIsRestricted) { + mRestriction = aMatrix.TransformRect(mRestriction); + } + } + + void TransformBoundsBy(const gfxMatrix& aMatrix) { + mRect = aMatrix.TransformBounds(mRect); + if (mIsRestricted) { + mRestriction = aMatrix.TransformBounds(mRestriction); + } + } + + ImageRegion operator-(const gfxPoint& aPt) const { + if (mIsRestricted) { + return CreateWithSamplingRestriction(mRect - aPt, mRestriction - aPt); + } + return Create(mRect - aPt); + } + + ImageRegion operator+(const gfxPoint& aPt) const { + if (mIsRestricted) { + return CreateWithSamplingRestriction(mRect + aPt, mRestriction + aPt); + } + return Create(mRect + aPt); + } + + gfx::ExtendMode GetExtendMode() const { return mExtendMode; } + + /* ImageRegion() : mIsRestricted(false) { } */ + + private: + explicit ImageRegion(const gfxRect& aRect, ExtendMode aExtendMode) + : mRect(aRect), mExtendMode(aExtendMode), mIsRestricted(false) {} + + ImageRegion(const gfxRect& aRect, const gfxRect& aRestriction, + ExtendMode aExtendMode) + : mRect(aRect), + mRestriction(aRestriction), + mExtendMode(aExtendMode), + mIsRestricted(true) {} + + gfxRect mRect; + gfxRect mRestriction; + ExtendMode mExtendMode; + bool mIsRestricted; +}; + +/** + * An axis-aligned rectangle in tiled image space, with an optional sampling + * restriction rect. The drawing code ensures that if a sampling restriction + * rect is present, any pixels sampled during the drawing process are found + * within that rect. + * + * The sampling restriction rect exists primarily for callers which perform + * pixel snapping. Other callers should generally use one of the Create() + * overloads. + */ +class ImageIntRegion { + typedef mozilla::gfx::ExtendMode ExtendMode; + + public: + static ImageIntRegion Empty() { + return ImageIntRegion(mozilla::gfx::IntRect(), ExtendMode::CLAMP); + } + + static ImageIntRegion Create(const mozilla::gfx::IntRect& aRect, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageIntRegion(aRect, aExtendMode); + } + + static ImageIntRegion Create(const mozilla::gfx::IntSize& aSize, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageIntRegion( + mozilla::gfx::IntRect(0, 0, aSize.width, aSize.height), aExtendMode); + } + + static ImageIntRegion CreateWithSamplingRestriction( + const mozilla::gfx::IntRect& aRect, + const mozilla::gfx::IntRect& aRestriction, + ExtendMode aExtendMode = ExtendMode::CLAMP) { + return ImageIntRegion(aRect, aRestriction, aExtendMode); + } + + bool IsRestricted() const { return mIsRestricted; } + const mozilla::gfx::IntRect& Rect() const { return mRect; } + + const mozilla::gfx::IntRect& Restriction() const { + MOZ_ASSERT(mIsRestricted); + return mRestriction; + } + + bool RestrictionContains(const mozilla::gfx::IntRect& aRect) const { + if (!mIsRestricted) { + return true; + } + return mRestriction.Contains(aRect); + } + + ImageIntRegion Intersect(const mozilla::gfx::IntRect& aRect) const { + if (mIsRestricted) { + return CreateWithSamplingRestriction(aRect.Intersect(mRect), + aRect.Intersect(mRestriction)); + } + return Create(aRect.Intersect(mRect)); + } + + mozilla::gfx::IntRect IntersectAndRestrict( + const mozilla::gfx::IntRect& aRect) const { + mozilla::gfx::IntRect intersection = mRect.Intersect(aRect); + if (mIsRestricted) { + intersection = mRestriction.Intersect(intersection); + } + return intersection; + } + + gfx::ExtendMode GetExtendMode() const { return mExtendMode; } + + ImageRegion ToImageRegion() const { + if (mIsRestricted) { + return ImageRegion::CreateWithSamplingRestriction( + gfxRect(mRect.x, mRect.y, mRect.width, mRect.height), + gfxRect(mRestriction.x, mRestriction.y, mRestriction.width, + mRestriction.height), + mExtendMode); + } + return ImageRegion::Create( + gfxRect(mRect.x, mRect.y, mRect.width, mRect.height), mExtendMode); + } + + bool operator==(const ImageIntRegion& aOther) const { + return mExtendMode == aOther.mExtendMode && + mIsRestricted == aOther.mIsRestricted && + mRect.IsEqualEdges(aOther.mRect) && + (!mIsRestricted || mRestriction.IsEqualEdges(aOther.mRestriction)); + } + + PLDHashNumber Hash() const { + return HashGeneric(mRect.x, mRect.y, mRect.width, mRect.height, + mRestriction.x, mRestriction.y, mRestriction.width, + mRestriction.height, mExtendMode, mIsRestricted); + } + + /* ImageIntRegion() : mIsRestricted(false) { } */ + + private: + explicit ImageIntRegion(const mozilla::gfx::IntRect& aRect, + ExtendMode aExtendMode) + : mRect(aRect), mExtendMode(aExtendMode), mIsRestricted(false) {} + + ImageIntRegion(const mozilla::gfx::IntRect& aRect, + const mozilla::gfx::IntRect& aRestriction, + ExtendMode aExtendMode) + : mRect(aRect), + mRestriction(aRestriction), + mExtendMode(aExtendMode), + mIsRestricted(true) {} + + mozilla::gfx::IntRect mRect; + mozilla::gfx::IntRect mRestriction; + ExtendMode mExtendMode; + bool mIsRestricted; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageRegion_h diff --git a/image/ImageWrapper.cpp b/image/ImageWrapper.cpp new file mode 100644 index 0000000000..eafd6ffe04 --- /dev/null +++ b/image/ImageWrapper.cpp @@ -0,0 +1,289 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageWrapper.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" +#include "Orientation.h" +#include "mozilla/image/Resolution.h" + +#include "mozilla/MemoryReporting.h" + +namespace mozilla { + +using dom::Document; +using gfx::IntSize; +using gfx::SamplingFilter; +using gfx::SourceSurface; +using layers::ImageContainer; + +namespace image { + +// Inherited methods from Image. + +already_AddRefed ImageWrapper::GetProgressTracker() { + return mInnerImage->GetProgressTracker(); +} + +size_t ImageWrapper::SizeOfSourceWithComputedFallback( + SizeOfState& aState) const { + return mInnerImage->SizeOfSourceWithComputedFallback(aState); +} + +void ImageWrapper::CollectSizeOfSurfaces( + nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const { + mInnerImage->CollectSizeOfSurfaces(aCounters, aMallocSizeOf); +} + +void ImageWrapper::IncrementAnimationConsumers() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread only to encourage serialization " + "with DecrementAnimationConsumers"); + mInnerImage->IncrementAnimationConsumers(); +} + +void ImageWrapper::DecrementAnimationConsumers() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread only to encourage serialization " + "with IncrementAnimationConsumers"); + mInnerImage->DecrementAnimationConsumers(); +} + +#ifdef DEBUG +uint32_t ImageWrapper::GetAnimationConsumers() { + return mInnerImage->GetAnimationConsumers(); +} +#endif + +nsresult ImageWrapper::OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) { + return mInnerImage->OnImageDataAvailable(aRequest, aInStr, aSourceOffset, + aCount); +} + +nsresult ImageWrapper::OnImageDataComplete(nsIRequest* aRequest, + nsresult aStatus, bool aLastPart) { + return mInnerImage->OnImageDataComplete(aRequest, aStatus, aLastPart); +} + +void ImageWrapper::OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) { + return mInnerImage->OnSurfaceDiscarded(aSurfaceKey); +} + +void ImageWrapper::SetInnerWindowID(uint64_t aInnerWindowId) { + mInnerImage->SetInnerWindowID(aInnerWindowId); +} + +uint64_t ImageWrapper::InnerWindowID() const { + return mInnerImage->InnerWindowID(); +} + +bool ImageWrapper::HasError() { return mInnerImage->HasError(); } + +void ImageWrapper::SetHasError() { mInnerImage->SetHasError(); } + +nsIURI* ImageWrapper::GetURI() const { return mInnerImage->GetURI(); } + +// Methods inherited from XPCOM interfaces. + +NS_IMPL_ISUPPORTS(ImageWrapper, imgIContainer) + +NS_IMETHODIMP +ImageWrapper::GetWidth(int32_t* aWidth) { + return mInnerImage->GetWidth(aWidth); +} + +NS_IMETHODIMP +ImageWrapper::GetHeight(int32_t* aHeight) { + return mInnerImage->GetHeight(aHeight); +} + +void ImageWrapper::MediaFeatureValuesChangedAllDocuments( + const mozilla::MediaFeatureChange& aChange) {} + +nsresult ImageWrapper::GetNativeSizes(nsTArray& aNativeSizes) { + return mInnerImage->GetNativeSizes(aNativeSizes); +} + +size_t ImageWrapper::GetNativeSizesLength() { + return mInnerImage->GetNativeSizesLength(); +} + +NS_IMETHODIMP +ImageWrapper::GetIntrinsicSize(nsSize* aSize) { + return mInnerImage->GetIntrinsicSize(aSize); +} + +Maybe ImageWrapper::GetIntrinsicRatio() { + return mInnerImage->GetIntrinsicRatio(); +} + +nsresult ImageWrapper::GetHotspotX(int32_t* aX) { + return Image::GetHotspotX(aX); +} + +nsresult ImageWrapper::GetHotspotY(int32_t* aY) { + return Image::GetHotspotY(aY); +} + +NS_IMETHODIMP_(Orientation) +ImageWrapper::GetOrientation() { return mInnerImage->GetOrientation(); } + +NS_IMETHODIMP_(Resolution) +ImageWrapper::GetResolution() { return mInnerImage->GetResolution(); } + +NS_IMETHODIMP +ImageWrapper::GetType(uint16_t* aType) { return mInnerImage->GetType(aType); } + +NS_IMETHODIMP +ImageWrapper::GetProviderId(uint32_t* aId) { + return mInnerImage->GetProviderId(aId); +} + +NS_IMETHODIMP +ImageWrapper::GetAnimated(bool* aAnimated) { + return mInnerImage->GetAnimated(aAnimated); +} + +NS_IMETHODIMP_(already_AddRefed) +ImageWrapper::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + return mInnerImage->GetFrame(aWhichFrame, aFlags); +} + +NS_IMETHODIMP_(already_AddRefed) +ImageWrapper::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + return mInnerImage->GetFrameAtSize(aSize, aWhichFrame, aFlags); +} + +NS_IMETHODIMP_(bool) +ImageWrapper::WillDrawOpaqueNow() { return mInnerImage->WillDrawOpaqueNow(); } + +NS_IMETHODIMP_(bool) +ImageWrapper::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + return mInnerImage->IsImageContainerAvailable(aRenderer, aFlags); +} + +NS_IMETHODIMP_(ImgDrawResult) +ImageWrapper::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + return mInnerImage->GetImageProvider(aRenderer, aSize, aSVGContext, aRegion, + aFlags, aProvider); +} + +NS_IMETHODIMP_(ImgDrawResult) +ImageWrapper::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + return mInnerImage->Draw(aContext, aSize, aRegion, aWhichFrame, + aSamplingFilter, aSVGContext, aFlags, aOpacity); +} + +NS_IMETHODIMP +ImageWrapper::StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) { + return mInnerImage->StartDecoding(aFlags, aWhichFrame); +} + +bool ImageWrapper::StartDecodingWithResult(uint32_t aFlags, + uint32_t aWhichFrame) { + return mInnerImage->StartDecodingWithResult(aFlags, aWhichFrame); +} + +bool ImageWrapper::HasDecodedPixels() { + return InnerImage()->HasDecodedPixels(); +} + +imgIContainer::DecodeResult ImageWrapper::RequestDecodeWithResult( + uint32_t aFlags, uint32_t aWhichFrame) { + return mInnerImage->RequestDecodeWithResult(aFlags, aWhichFrame); +} + +NS_IMETHODIMP +ImageWrapper::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) { + return mInnerImage->RequestDecodeForSize(aSize, aFlags, aWhichFrame); +} + +NS_IMETHODIMP +ImageWrapper::LockImage() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread to encourage serialization with UnlockImage"); + return mInnerImage->LockImage(); +} + +NS_IMETHODIMP +ImageWrapper::UnlockImage() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread to encourage serialization with LockImage"); + return mInnerImage->UnlockImage(); +} + +NS_IMETHODIMP +ImageWrapper::RequestDiscard() { return mInnerImage->RequestDiscard(); } + +NS_IMETHODIMP_(void) +ImageWrapper::RequestRefresh(const TimeStamp& aTime) { + return mInnerImage->RequestRefresh(aTime); +} + +NS_IMETHODIMP +ImageWrapper::GetAnimationMode(uint16_t* aAnimationMode) { + return mInnerImage->GetAnimationMode(aAnimationMode); +} + +NS_IMETHODIMP +ImageWrapper::SetAnimationMode(uint16_t aAnimationMode) { + return mInnerImage->SetAnimationMode(aAnimationMode); +} + +NS_IMETHODIMP +ImageWrapper::ResetAnimation() { return mInnerImage->ResetAnimation(); } + +NS_IMETHODIMP_(float) +ImageWrapper::GetFrameIndex(uint32_t aWhichFrame) { + return mInnerImage->GetFrameIndex(aWhichFrame); +} + +NS_IMETHODIMP_(int32_t) +ImageWrapper::GetFirstFrameDelay() { return mInnerImage->GetFirstFrameDelay(); } + +NS_IMETHODIMP_(void) +ImageWrapper::SetAnimationStartTime(const TimeStamp& aTime) { + mInnerImage->SetAnimationStartTime(aTime); +} + +void ImageWrapper::PropagateUseCounters(Document* aReferencingDocument) { + mInnerImage->PropagateUseCounters(aReferencingDocument); +} + +nsIntSize ImageWrapper::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + return mInnerImage->OptimalImageSizeForDest(aDest, aWhichFrame, + aSamplingFilter, aFlags); +} + +NS_IMETHODIMP_(nsIntRect) +ImageWrapper::GetImageSpaceInvalidationRect(const nsIntRect& aRect) { + return mInnerImage->GetImageSpaceInvalidationRect(aRect); +} + +already_AddRefed ImageWrapper::Unwrap() { + return mInnerImage->Unwrap(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/ImageWrapper.h b/image/ImageWrapper.h new file mode 100644 index 0000000000..d72393e976 --- /dev/null +++ b/image/ImageWrapper.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImageWrapper_h +#define mozilla_image_ImageWrapper_h + +#include "mozilla/MemoryReporting.h" +#include "Image.h" + +namespace mozilla { +namespace image { + +/** + * Abstract superclass for Images that wrap other Images. + */ +class ImageWrapper : public Image { + public: + NS_DECL_ISUPPORTS + NS_DECL_IMGICONTAINER + + // Inherited methods from Image. + virtual already_AddRefed GetProgressTracker() override; + + virtual size_t SizeOfSourceWithComputedFallback( + SizeOfState& aState) const override; + virtual void CollectSizeOfSurfaces(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) const override; + + virtual void IncrementAnimationConsumers() override; + virtual void DecrementAnimationConsumers() override; +#ifdef DEBUG + virtual uint32_t GetAnimationConsumers() override; +#endif + + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) override; + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aStatus, + bool aLastPart) override; + + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override; + + virtual void SetInnerWindowID(uint64_t aInnerWindowId) override; + virtual uint64_t InnerWindowID() const override; + + virtual bool HasError() override; + virtual void SetHasError() override; + + nsIURI* GetURI() const override; + + protected: + explicit ImageWrapper(Image* aInnerImage) : mInnerImage(aInnerImage) { + MOZ_ASSERT(aInnerImage, "Need an image to wrap"); + } + + virtual ~ImageWrapper() {} + + /** + * Returns a weak reference to the inner image wrapped by this ImageWrapper. + */ + Image* InnerImage() const { return mInnerImage.get(); } + + void SetInnerImage(Image* aInnerImage) { + MOZ_ASSERT(aInnerImage, "Need an image to wrap"); + mInnerImage = aInnerImage; + } + + private: + RefPtr mInnerImage; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImageWrapper_h diff --git a/image/ImgDrawResult.h b/image/ImgDrawResult.h new file mode 100644 index 0000000000..d33b63d71c --- /dev/null +++ b/image/ImgDrawResult.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ImgDrawResult_h +#define mozilla_image_ImgDrawResult_h + +#include // for uint8_t +#include "mozilla/Attributes.h" +#include "mozilla/Likely.h" + +namespace mozilla { +namespace image { + +/** + * An enumeration representing the result of a drawing operation. + * + * Most users of ImgDrawResult will only be interested in whether the value is + * SUCCESS or not. The other values are primarily useful for debugging and error + * handling. + * + * SUCCESS: We successfully drew a completely decoded frame of the requested + * size. Drawing again with FLAG_SYNC_DECODE would not change the result. + * + * SUCCESS_NOT_COMPLETE: The image was drawn successfully and completely, but + * it hasn't notified about the sync-decode yet. This can only happen when + * layout pokes at the internal image state beforehand via the result of + * StartDecodingWithResult. This should probably go away eventually, somehow, + * see bug 1471583. + * + * INCOMPLETE: We successfully drew a frame that was partially decoded. (Note + * that successfully drawing a partially decoded frame may not actually draw any + * pixels!) Drawing again with FLAG_SYNC_DECODE would improve the result. + * + * WRONG_SIZE: We successfully drew a wrongly-sized frame that had to be scaled. + * This is only returned if drawing again with FLAG_SYNC_DECODE would improve + * the result; if the size requested was larger than the intrinsic size of the + * image, for example, we would generally have to scale whether FLAG_SYNC_DECODE + * was specified or not, and therefore we would not return WRONG_SIZE. + * + * NOT_READY: We failed to draw because no decoded version of the image was + * available. Drawing again with FLAG_SYNC_DECODE would improve the result. + * (Though FLAG_SYNC_DECODE will not necessarily work until after the image's + * load event!) + * + * TEMPORARY_ERROR: We failed to draw due to a temporary error. Drawing may + * succeed at a later time. + * + * BAD_IMAGE: We failed to draw because the image has an error. This is a + * permanent condition. + * + * BAD_ARGS: We failed to draw because bad arguments were passed to draw(). + * + * NOT_SUPPORTED: The requested operation is not supported, but the image is + * otherwise valid. + */ +enum class [[nodiscard]] ImgDrawResult : uint8_t { + SUCCESS, + SUCCESS_NOT_COMPLETE, + INCOMPLETE, + WRONG_SIZE, + NOT_READY, + TEMPORARY_ERROR, + BAD_IMAGE, + BAD_ARGS, + NOT_SUPPORTED +}; + +/** + * You can combine ImgDrawResults with &. By analogy to bitwise-&, the result is + * ImgDrawResult::SUCCESS only if both operands are ImgDrawResult::SUCCESS. + * Otherwise, a failing ImgDrawResult is returned; we favor the left operand's + * failure when deciding which failure to return, with the exception that we + * always prefer any other kind of failure over ImgDrawResult::BAD_IMAGE, since + * other failures are recoverable and we want to know if any recoverable + * failures occurred. + */ +inline ImgDrawResult operator&(const ImgDrawResult aLeft, + const ImgDrawResult aRight) { + if (MOZ_LIKELY(aLeft == ImgDrawResult::SUCCESS)) { + return aRight; + } + + if (aLeft == ImgDrawResult::NOT_SUPPORTED || + aRight == ImgDrawResult::NOT_SUPPORTED) { + return ImgDrawResult::NOT_SUPPORTED; + } + + if ((aLeft == ImgDrawResult::BAD_IMAGE || + aLeft == ImgDrawResult::SUCCESS_NOT_COMPLETE) && + aRight != ImgDrawResult::SUCCESS && + aRight != ImgDrawResult::SUCCESS_NOT_COMPLETE) { + return aRight; + } + return aLeft; +} + +inline ImgDrawResult& operator&=(ImgDrawResult& aLeft, + const ImgDrawResult aRight) { + aLeft = aLeft & aRight; + return aLeft; +} + +/** + * A struct used during painting to provide input flags to determine how + * imagelib draw calls should behave and an output ImgDrawResult to return + * information about the result of any imagelib draw calls that may have + * occurred. + */ +struct imgDrawingParams { + explicit imgDrawingParams(uint32_t aImageFlags = 0) + : imageFlags(aImageFlags), result(ImgDrawResult::SUCCESS) {} + + const uint32_t imageFlags; // imgIContainer::FLAG_* image flags to pass to + // image lib draw calls. + ImgDrawResult result; // To return results from image lib painting. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ImgDrawResult_h diff --git a/image/LookupResult.h b/image/LookupResult.h new file mode 100644 index 0000000000..bbd1cca2a7 --- /dev/null +++ b/image/LookupResult.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * LookupResult is the return type of SurfaceCache's Lookup*() functions. It + * combines a surface with relevant metadata tracked by SurfaceCache. + */ + +#ifndef mozilla_image_LookupResult_h +#define mozilla_image_LookupResult_h + +#include + +#include "ISurfaceProvider.h" +#include "mozilla/Attributes.h" +#include "mozilla/gfx/Point.h" // for IntSize + +namespace mozilla { +namespace image { + +enum class MatchType : uint8_t { + NOT_FOUND, // No matching surface and no placeholder. + PENDING, // Found a matching placeholder, but no surface. + EXACT, // Found a surface that matches exactly. + SUBSTITUTE_BECAUSE_NOT_FOUND, // No exact match, but found a similar one. + SUBSTITUTE_BECAUSE_PENDING, // Found a similar surface and a placeholder + // for an exact match. + + /* No exact match, but this should be considered an exact match for purposes + * of deciding whether or not to request a new decode. This is because the + * cache has determined that callers require too many size variants of this + * image. It determines the set of sizes which best represent the image, and + * will only suggest decoding of unavailable sizes from that set. */ + SUBSTITUTE_BECAUSE_BEST +}; + +/** + * LookupResult is the return type of SurfaceCache's Lookup*() functions. It + * combines a surface with relevant metadata tracked by SurfaceCache. + */ +class MOZ_STACK_CLASS LookupResult { + public: + explicit LookupResult(MatchType aMatchType) + : mMatchType(aMatchType), mFailedToRequestDecode(false) { + MOZ_ASSERT( + mMatchType == MatchType::NOT_FOUND || mMatchType == MatchType::PENDING, + "Only NOT_FOUND or PENDING make sense with no surface"); + } + + LookupResult(LookupResult&& aOther) + : mSurface(std::move(aOther.mSurface)), + mMatchType(aOther.mMatchType), + mSuggestedSize(aOther.mSuggestedSize), + mFailedToRequestDecode(aOther.mFailedToRequestDecode) {} + + LookupResult(DrawableSurface&& aSurface, MatchType aMatchType) + : mSurface(std::move(aSurface)), + mMatchType(aMatchType), + mFailedToRequestDecode(false) { + MOZ_ASSERT(!mSurface || !(mMatchType == MatchType::NOT_FOUND || + mMatchType == MatchType::PENDING), + "Only NOT_FOUND or PENDING make sense with no surface"); + MOZ_ASSERT(mSurface || mMatchType == MatchType::NOT_FOUND || + mMatchType == MatchType::PENDING, + "NOT_FOUND or PENDING do not make sense with a surface"); + } + + LookupResult(MatchType aMatchType, const gfx::IntSize& aSuggestedSize) + : mMatchType(aMatchType), + mSuggestedSize(aSuggestedSize), + mFailedToRequestDecode(false) { + MOZ_ASSERT( + mMatchType == MatchType::NOT_FOUND || mMatchType == MatchType::PENDING, + "Only NOT_FOUND or PENDING make sense with no surface"); + } + + LookupResult(DrawableSurface&& aSurface, MatchType aMatchType, + const gfx::IntSize& aSuggestedSize) + : mSurface(std::move(aSurface)), + mMatchType(aMatchType), + mSuggestedSize(aSuggestedSize), + mFailedToRequestDecode(false) { + MOZ_ASSERT(!mSurface || !(mMatchType == MatchType::NOT_FOUND || + mMatchType == MatchType::PENDING), + "Only NOT_FOUND or PENDING make sense with no surface"); + MOZ_ASSERT(mSurface || mMatchType == MatchType::NOT_FOUND || + mMatchType == MatchType::PENDING, + "NOT_FOUND or PENDING do not make sense with a surface"); + } + + LookupResult& operator=(LookupResult&& aOther) { + MOZ_ASSERT(&aOther != this, "Self-move-assignment is not supported"); + mSurface = std::move(aOther.mSurface); + mMatchType = aOther.mMatchType; + mSuggestedSize = aOther.mSuggestedSize; + mFailedToRequestDecode = aOther.mFailedToRequestDecode; + return *this; + } + + DrawableSurface& Surface() { return mSurface; } + const DrawableSurface& Surface() const { return mSurface; } + const gfx::IntSize& SuggestedSize() const { return mSuggestedSize; } + + /// @return true if this LookupResult contains a surface. + explicit operator bool() const { return bool(mSurface); } + + /// @return what kind of match this is (exact, substitute, etc.) + MatchType Type() const { return mMatchType; } + + void SetFailedToRequestDecode() { mFailedToRequestDecode = true; } + bool GetFailedToRequestDecode() { return mFailedToRequestDecode; } + + private: + LookupResult(const LookupResult&) = delete; + LookupResult& operator=(const LookupResult& aOther) = delete; + + DrawableSurface mSurface; + MatchType mMatchType; + + /// mSuggestedSize will be the size of the returned surface if the result is + /// SUBSTITUTE_BECAUSE_BEST. It will be empty for EXACT, and can contain a + /// non-empty size possibly different from the returned surface (if any) for + /// all other results. If non-empty, it will always be the size the caller + /// should request any decodes at. + gfx::IntSize mSuggestedSize; + + // True if we tried to start a decode but failed, likely because the image was + // too big to fit into the surface cache. + bool mFailedToRequestDecode; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_LookupResult_h diff --git a/image/MultipartImage.cpp b/image/MultipartImage.cpp new file mode 100644 index 0000000000..a52b87b099 --- /dev/null +++ b/image/MultipartImage.cpp @@ -0,0 +1,300 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MultipartImage.h" + +#include "imgINotificationObserver.h" + +namespace mozilla { + +using gfx::IntSize; +using gfx::SourceSurface; + +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// Helpers +/////////////////////////////////////////////////////////////////////////////// + +class NextPartObserver : public IProgressObserver { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(NextPartObserver) + NS_INLINE_DECL_REFCOUNTING(NextPartObserver, override) + + explicit NextPartObserver(MultipartImage* aOwner) : mOwner(aOwner) { + MOZ_ASSERT(mOwner); + } + + void BeginObserving(Image* aImage) { + MOZ_ASSERT(aImage); + mImage = aImage; + + RefPtr tracker = mImage->GetProgressTracker(); + tracker->AddObserver(this); + } + + void BlockUntilDecodedAndFinishObserving() { + // Use RequestDecodeForSize() to block until our image finishes decoding. + // The size is ignored because we don't pass the FLAG_HIGH_QUALITY_SCALING + // flag. + mImage->RequestDecodeForSize(gfx::IntSize(0, 0), + imgIContainer::FLAG_SYNC_DECODE); + + // RequestDecodeForSize() should've sent synchronous notifications that + // would have caused us to call FinishObserving() (and null out mImage) + // already. If for some reason it didn't, we should do so here. + if (mImage) { + FinishObserving(); + } + } + + virtual void Notify(int32_t aType, + const nsIntRect* aRect = nullptr) override { + if (!mImage) { + // We've already finished observing the last image we were given. + return; + } + + if (aType == imgINotificationObserver::FRAME_COMPLETE) { + FinishObserving(); + } + } + + virtual void OnLoadComplete(bool aLastPart) override { + if (!mImage) { + // We've already finished observing the last image we were given. + return; + } + + // Retrieve the image's intrinsic size. + int32_t width = 0; + int32_t height = 0; + mImage->GetWidth(&width); + mImage->GetHeight(&height); + + // Request decoding at the intrinsic size. + mImage->RequestDecodeForSize(IntSize(width, height), + imgIContainer::DECODE_FLAGS_DEFAULT | + imgIContainer::FLAG_HIGH_QUALITY_SCALING); + + // If there's already an error, we may never get a FRAME_COMPLETE + // notification, so go ahead and notify our owner right away. + RefPtr tracker = mImage->GetProgressTracker(); + if (tracker->GetProgress() & FLAG_HAS_ERROR) { + FinishObserving(); + } + } + + // Other notifications are ignored. + virtual void SetHasImage() override {} + virtual bool NotificationsDeferred() const override { return false; } + virtual void MarkPendingNotify() override {} + virtual void ClearPendingNotify() override {} + + private: + virtual ~NextPartObserver() {} + + void FinishObserving() { + MOZ_ASSERT(mImage); + + RefPtr tracker = mImage->GetProgressTracker(); + tracker->RemoveObserver(this); + mImage = nullptr; + + mOwner->FinishTransition(); + } + + MultipartImage* mOwner; + RefPtr mImage; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Implementation +/////////////////////////////////////////////////////////////////////////////// + +MultipartImage::MultipartImage(Image* aFirstPart) + : ImageWrapper(aFirstPart), mPendingNotify(false) { + mNextPartObserver = new NextPartObserver(this); +} + +void MultipartImage::Init() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mTracker, "Should've called SetProgressTracker() by now"); + + // Start observing the first part. + RefPtr firstPartTracker = InnerImage()->GetProgressTracker(); + firstPartTracker->AddObserver(this); + InnerImage()->IncrementAnimationConsumers(); +} + +MultipartImage::~MultipartImage() { + // Ask our ProgressTracker to drop its weak reference to us. + mTracker->ResetImage(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(MultipartImage, ImageWrapper) + +void MultipartImage::BeginTransitionToPart(Image* aNextPart) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aNextPart); + + if (mNextPart) { + // Let the decoder catch up so we don't drop frames. + mNextPartObserver->BlockUntilDecodedAndFinishObserving(); + MOZ_ASSERT(!mNextPart); + } + + mNextPart = aNextPart; + + // Start observing the next part; we'll complete the transition when + // NextPartObserver calls FinishTransition. + mNextPartObserver->BeginObserving(mNextPart); + mNextPart->IncrementAnimationConsumers(); +} + +static Progress FilterProgress(Progress aProgress) { + // Filter out onload blocking notifications, since we don't want to block + // onload for multipart images. + // Filter out errors, since we don't want errors in one part to error out + // the whole stream. + return aProgress & ~FLAG_HAS_ERROR; +} + +void MultipartImage::FinishTransition() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNextPart, "Should have a next part here"); + + RefPtr newCurrentPartTracker = + mNextPart->GetProgressTracker(); + if (newCurrentPartTracker->GetProgress() & FLAG_HAS_ERROR) { + // This frame has an error; drop it. + mNextPart = nullptr; + + // We still need to notify, though. + mTracker->ResetForNewRequest(); + RefPtr currentPartTracker = + InnerImage()->GetProgressTracker(); + mTracker->SyncNotifyProgress( + FilterProgress(currentPartTracker->GetProgress())); + + return; + } + + // Stop observing the current part. + { + RefPtr currentPartTracker = + InnerImage()->GetProgressTracker(); + currentPartTracker->RemoveObserver(this); + } + + // Make the next part become the current part. + mTracker->ResetForNewRequest(); + SetInnerImage(mNextPart); + mNextPart = nullptr; + newCurrentPartTracker->AddObserver(this); + + // Finally, send all the notifications for the new current part and send a + // FRAME_UPDATE notification so that observers know to redraw. + mTracker->SyncNotifyProgress( + FilterProgress(newCurrentPartTracker->GetProgress()), + GetMaxSizedIntRect()); +} + +already_AddRefed MultipartImage::Unwrap() { + // Although we wrap another image, we don't allow callers to unwrap as. As far + // as external code is concerned, MultipartImage is atomic. + nsCOMPtr image = this; + return image.forget(); +} + +already_AddRefed MultipartImage::GetProgressTracker() { + MOZ_ASSERT(mTracker); + RefPtr tracker = mTracker; + return tracker.forget(); +} + +void MultipartImage::SetProgressTracker(ProgressTracker* aTracker) { + MOZ_ASSERT(aTracker); + MOZ_ASSERT(!mTracker); + mTracker = aTracker; +} + +nsresult MultipartImage::OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) { + // Note that this method is special in that we forward it to the next part if + // one exists, and *not* the current part. + + // We may trigger notifications that will free mNextPart, so keep it alive. + RefPtr nextPart = mNextPart; + if (nextPart) { + nextPart->OnImageDataAvailable(aRequest, aInStr, aSourceOffset, aCount); + } else { + InnerImage()->OnImageDataAvailable(aRequest, aInStr, aSourceOffset, aCount); + } + + return NS_OK; +} + +nsresult MultipartImage::OnImageDataComplete(nsIRequest* aRequest, + nsresult aStatus, bool aLastPart) { + // Note that this method is special in that we forward it to the next part if + // one exists, and *not* the current part. + + // We may trigger notifications that will free mNextPart, so keep it alive. + RefPtr nextPart = mNextPart; + if (nextPart) { + nextPart->OnImageDataComplete(aRequest, aStatus, aLastPart); + } else { + InnerImage()->OnImageDataComplete(aRequest, aStatus, aLastPart); + } + + return NS_OK; +} + +void MultipartImage::Notify(int32_t aType, + const nsIntRect* aRect /* = nullptr*/) { + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + mTracker->SyncNotifyProgress(FLAG_SIZE_AVAILABLE); + } else if (aType == imgINotificationObserver::FRAME_UPDATE) { + mTracker->SyncNotifyProgress(NoProgress, *aRect); + } else if (aType == imgINotificationObserver::FRAME_COMPLETE) { + mTracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE); + } else if (aType == imgINotificationObserver::LOAD_COMPLETE) { + mTracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + } else if (aType == imgINotificationObserver::DECODE_COMPLETE) { + mTracker->SyncNotifyProgress(FLAG_DECODE_COMPLETE); + } else if (aType == imgINotificationObserver::DISCARD) { + mTracker->OnDiscard(); + } else if (aType == imgINotificationObserver::UNLOCKED_DRAW) { + mTracker->OnUnlockedDraw(); + } else if (aType == imgINotificationObserver::IS_ANIMATED) { + mTracker->SyncNotifyProgress(FLAG_IS_ANIMATED); + } else if (aType == imgINotificationObserver::HAS_TRANSPARENCY) { + mTracker->SyncNotifyProgress(FLAG_HAS_TRANSPARENCY); + } else { + MOZ_ASSERT_UNREACHABLE("Notification list should be exhaustive"); + } +} + +void MultipartImage::OnLoadComplete(bool aLastPart) { + Progress progress = FLAG_LOAD_COMPLETE; + if (aLastPart) { + progress |= FLAG_LAST_PART_COMPLETE; + } + mTracker->SyncNotifyProgress(progress); +} + +void MultipartImage::SetHasImage() { mTracker->OnImageAvailable(); } + +bool MultipartImage::NotificationsDeferred() const { return mPendingNotify; } + +void MultipartImage::MarkPendingNotify() { mPendingNotify = true; } + +void MultipartImage::ClearPendingNotify() { mPendingNotify = false; } + +} // namespace image +} // namespace mozilla diff --git a/image/MultipartImage.h b/image/MultipartImage.h new file mode 100644 index 0000000000..d4dbb845c0 --- /dev/null +++ b/image/MultipartImage.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_MultipartImage_h +#define mozilla_image_MultipartImage_h + +#include "ImageWrapper.h" +#include "IProgressObserver.h" +#include "ProgressTracker.h" + +namespace mozilla { +namespace image { + +class NextPartObserver; + +/** + * An Image wrapper that implements support for multipart/x-mixed-replace + * images. + */ +class MultipartImage : public ImageWrapper, public IProgressObserver { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(MultipartImage) + // We need to always declare refcounting here, because + // IProgressObserver has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + void BeginTransitionToPart(Image* aNextPart); + + // Overridden ImageWrapper methods: + virtual already_AddRefed Unwrap() override; + virtual already_AddRefed GetProgressTracker() override; + virtual void SetProgressTracker(ProgressTracker* aTracker) override; + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) override; + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aStatus, + bool aLastPart) override; + + // We don't support locking or track animation consumers for individual parts, + // so we override these methods to do nothing. + NS_IMETHOD LockImage() override { return NS_OK; } + NS_IMETHOD UnlockImage() override { return NS_OK; } + virtual void IncrementAnimationConsumers() override {} + virtual void DecrementAnimationConsumers() override {} +#ifdef DEBUG + virtual uint32_t GetAnimationConsumers() override { return 1; } +#endif + + // Overridden IProgressObserver methods: + virtual void Notify(int32_t aType, const nsIntRect* aRect = nullptr) override; + virtual void OnLoadComplete(bool aLastPart) override; + virtual void SetHasImage() override; + virtual bool NotificationsDeferred() const override; + virtual void MarkPendingNotify() override; + virtual void ClearPendingNotify() override; + + protected: + virtual ~MultipartImage(); + + private: + friend class ImageFactory; + friend class NextPartObserver; + + explicit MultipartImage(Image* aFirstPart); + void Init(); + + void FinishTransition(); + + RefPtr mTracker; + RefPtr mNextPartObserver; + RefPtr mNextPart; + bool mPendingNotify : 1; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_MultipartImage_h diff --git a/image/Orientation.h b/image/Orientation.h new file mode 100644 index 0000000000..e36a468372 --- /dev/null +++ b/image/Orientation.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_Orientation_h +#define mozilla_image_Orientation_h + +#include +#include "mozilla/gfx/Rect.h" + +namespace mozilla { + +// Pixel values in an image considering orientation metadata, such as the size +// of an image as seen by consumers of the image. +// +// Any public methods on RasterImage that use untyped units are interpreted as +// oriented pixels. +struct OrientedPixel {}; +template <> +struct IsPixel : std::true_type {}; +typedef gfx::IntPointTyped OrientedIntPoint; +typedef gfx::IntSizeTyped OrientedIntSize; +typedef gfx::IntRectTyped OrientedIntRect; + +// Pixel values in an image ignoring orientation metadata, such as are stored +// in surfaces and the raw pixel data in the image. +struct UnorientedPixel {}; +template <> +struct IsPixel : std::true_type {}; +typedef gfx::IntPointTyped UnorientedIntPoint; +typedef gfx::IntSizeTyped UnorientedIntSize; +typedef gfx::IntRectTyped UnorientedIntRect; + +namespace image { + +enum class Angle : uint8_t { D0, D90, D180, D270 }; + +enum class Flip : uint8_t { Unflipped, Horizontal }; + +/** + * A struct that describes an image's orientation as a rotation optionally + * followed by a reflection. This may be used to be indicate an image's inherent + * orientation or a desired orientation for the image. + * + * When flipFirst = true, this indicates that the reflection is applied before + * the rotation. (This is used by OrientedImage to represent the inverse of an + * underlying image's Orientation.) + */ +struct Orientation { + explicit Orientation(Angle aRotation = Angle::D0, + Flip aFlip = Flip::Unflipped, bool aFlipFirst = false) + : rotation(aRotation), flip(aFlip), flipFirst(aFlipFirst) {} + + Orientation Reversed() const { + return Orientation(InvertAngle(rotation), flip, !flipFirst); + } + + bool IsIdentity() const { + return (rotation == Angle::D0) && (flip == Flip::Unflipped); + } + + bool SwapsWidthAndHeight() const { + return (rotation == Angle::D90) || (rotation == Angle::D270); + } + + bool operator==(const Orientation& aOther) const { + return rotation == aOther.rotation && flip == aOther.flip && + flipFirst == aOther.flipFirst; + } + + bool operator!=(const Orientation& aOther) const { + return !(*this == aOther); + } + + OrientedIntSize ToOriented(const UnorientedIntSize& aSize) const { + if (SwapsWidthAndHeight()) { + return OrientedIntSize(aSize.height, aSize.width); + } else { + return OrientedIntSize(aSize.width, aSize.height); + } + } + + UnorientedIntSize ToUnoriented(const OrientedIntSize& aSize) const { + if (SwapsWidthAndHeight()) { + return UnorientedIntSize(aSize.height, aSize.width); + } else { + return UnorientedIntSize(aSize.width, aSize.height); + } + } + + static Angle InvertAngle(Angle aAngle) { + switch (aAngle) { + case Angle::D90: + return Angle::D270; + case Angle::D270: + return Angle::D90; + default: + return aAngle; + } + } + + Angle rotation; + Flip flip; + bool flipFirst; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_Orientation_h diff --git a/image/OrientedImage.cpp b/image/OrientedImage.cpp new file mode 100644 index 0000000000..8461405dcb --- /dev/null +++ b/image/OrientedImage.cpp @@ -0,0 +1,349 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OrientedImage.h" + +#include + +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxDrawable.h" +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "ImageRegion.h" +#include "mozilla/SVGImageContext.h" + +using std::swap; + +namespace mozilla { + +using namespace gfx; + +namespace image { + +NS_IMETHODIMP +OrientedImage::GetWidth(int32_t* aWidth) { + if (mOrientation.SwapsWidthAndHeight()) { + return InnerImage()->GetHeight(aWidth); + } else { + return InnerImage()->GetWidth(aWidth); + } +} + +NS_IMETHODIMP +OrientedImage::GetHeight(int32_t* aHeight) { + if (mOrientation.SwapsWidthAndHeight()) { + return InnerImage()->GetWidth(aHeight); + } else { + return InnerImage()->GetHeight(aHeight); + } +} + +nsresult OrientedImage::GetNativeSizes(nsTArray& aNativeSizes) { + nsresult rv = InnerImage()->GetNativeSizes(aNativeSizes); + + if (mOrientation.SwapsWidthAndHeight()) { + auto i = aNativeSizes.Length(); + while (i > 0) { + --i; + swap(aNativeSizes[i].width, aNativeSizes[i].height); + } + } + + return rv; +} + +NS_IMETHODIMP +OrientedImage::GetIntrinsicSize(nsSize* aSize) { + nsresult rv = InnerImage()->GetIntrinsicSize(aSize); + + if (mOrientation.SwapsWidthAndHeight()) { + swap(aSize->width, aSize->height); + } + + return rv; +} + +Maybe OrientedImage::GetIntrinsicRatio() { + Maybe ratio = InnerImage()->GetIntrinsicRatio(); + if (ratio && mOrientation.SwapsWidthAndHeight()) { + ratio = Some(ratio->Inverted()); + } + return ratio; +} + +already_AddRefed OrientedImage::OrientSurface( + Orientation aOrientation, SourceSurface* aSurface) { + MOZ_ASSERT(aSurface); + + // If the image does not require any re-orientation, return aSurface itself. + if (aOrientation.IsIdentity()) { + return do_AddRef(aSurface); + } + + // Determine the size of the new surface. + nsIntSize originalSize = aSurface->GetSize(); + nsIntSize targetSize = originalSize; + if (aOrientation.SwapsWidthAndHeight()) { + swap(targetSize.width, targetSize.height); + } + + // Create our drawable. + RefPtr drawable = new gfxSurfaceDrawable(aSurface, originalSize); + + // Determine an appropriate format for the surface. + gfx::SurfaceFormat surfaceFormat = IsOpaque(aSurface->GetFormat()) + ? gfx::SurfaceFormat::OS_RGBX + : gfx::SurfaceFormat::OS_RGBA; + + // Create the new surface to draw into. + RefPtr target = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + targetSize, surfaceFormat); + if (!target || !target->IsValid()) { + NS_ERROR("Could not create a DrawTarget"); + return nullptr; + } + + // Draw. + gfxContext ctx(target); + + ctx.Multiply(OrientationMatrix(aOrientation, originalSize)); + gfxUtils::DrawPixelSnapped(&ctx, drawable, SizeDouble(originalSize), + ImageRegion::Create(originalSize), surfaceFormat, + SamplingFilter::LINEAR); + + return target->Snapshot(); +} + +NS_IMETHODIMP_(already_AddRefed) +OrientedImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + // Get a SourceSurface for the inner image then orient it according to + // mOrientation. + RefPtr innerSurface = + InnerImage()->GetFrame(aWhichFrame, aFlags); + NS_ENSURE_TRUE(innerSurface, nullptr); + + return OrientSurface(mOrientation, innerSurface); +} + +NS_IMETHODIMP_(already_AddRefed) +OrientedImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + // Get a SourceSurface for the inner image then orient it according to + // mOrientation. + IntSize innerSize = aSize; + if (mOrientation.SwapsWidthAndHeight()) { + swap(innerSize.width, innerSize.height); + } + RefPtr innerSurface = + InnerImage()->GetFrameAtSize(innerSize, aWhichFrame, aFlags); + NS_ENSURE_TRUE(innerSurface, nullptr); + + return OrientSurface(mOrientation, innerSurface); +} + +NS_IMETHODIMP_(bool) +OrientedImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + if (mOrientation.IsIdentity()) { + return InnerImage()->IsImageContainerAvailable(aRenderer, aFlags); + } + return false; +} + +NS_IMETHODIMP_(ImgDrawResult) +OrientedImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + // XXX(seth): We currently don't have a way of orienting the result of + // GetImageContainer. We work around this by always returning null, but if it + // ever turns out that OrientedImage is widely used on codepaths that can + // actually benefit from GetImageContainer, it would be a good idea to fix + // that method for performance reasons. + + if (mOrientation.IsIdentity()) { + return InnerImage()->GetImageProvider(aRenderer, aSize, aSVGContext, + aRegion, aFlags, aProvider); + } + + return ImgDrawResult::NOT_SUPPORTED; +} + +struct MatrixBuilder { + explicit MatrixBuilder(bool aInvert) : mInvert(aInvert) {} + + gfxMatrix Build() { return mMatrix; } + + void Scale(gfxFloat aX, gfxFloat aY) { + if (mInvert) { + mMatrix *= gfxMatrix::Scaling(1.0 / aX, 1.0 / aY); + } else { + mMatrix.PreScale(aX, aY); + } + } + + void Rotate(gfxFloat aPhi) { + if (mInvert) { + mMatrix *= gfxMatrix::Rotation(-aPhi); + } else { + mMatrix.PreRotate(aPhi); + } + } + + void Translate(gfxPoint aDelta) { + if (mInvert) { + mMatrix *= gfxMatrix::Translation(-aDelta); + } else { + mMatrix.PreTranslate(aDelta); + } + } + + private: + gfxMatrix mMatrix; + bool mInvert; +}; + +gfxMatrix OrientedImage::OrientationMatrix(Orientation aOrientation, + const nsIntSize& aSize, + bool aInvert /* = false */) { + MatrixBuilder builder(aInvert); + + // Apply reflection, if present. (For a regular, non-flipFirst reflection, + // this logically happens second, but we apply it first because these + // transformations are all premultiplied.) A translation is necessary to place + // the image back in the first quadrant. + if (aOrientation.flip == Flip::Horizontal && !aOrientation.flipFirst) { + if (aOrientation.SwapsWidthAndHeight()) { + builder.Translate(gfxPoint(aSize.height, 0)); + } else { + builder.Translate(gfxPoint(aSize.width, 0)); + } + builder.Scale(-1.0, 1.0); + } + + // Apply rotation, if present. Again, a translation is used to place the + // image back in the first quadrant. + switch (aOrientation.rotation) { + case Angle::D0: + break; + case Angle::D90: + builder.Translate(gfxPoint(aSize.height, 0)); + builder.Rotate(-1.5 * M_PI); + break; + case Angle::D180: + builder.Translate(gfxPoint(aSize.width, aSize.height)); + builder.Rotate(-1.0 * M_PI); + break; + case Angle::D270: + builder.Translate(gfxPoint(0, aSize.width)); + builder.Rotate(-0.5 * M_PI); + break; + default: + MOZ_ASSERT(false, "Invalid rotation value"); + } + + // Apply a flipFirst reflection. + if (aOrientation.flip == Flip::Horizontal && aOrientation.flipFirst) { + builder.Translate(gfxPoint(aSize.width, 0.0)); + builder.Scale(-1.0, 1.0); + } + + return builder.Build(); +} + +NS_IMETHODIMP_(ImgDrawResult) +OrientedImage::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + if (mOrientation.IsIdentity()) { + return InnerImage()->Draw(aContext, aSize, aRegion, aWhichFrame, + aSamplingFilter, aSVGContext, aFlags, aOpacity); + } + + // Update the image size to match the image's coordinate system. (This could + // be done using TransformBounds but since it's only a size a swap is enough.) + nsIntSize size(aSize); + if (mOrientation.SwapsWidthAndHeight()) { + swap(size.width, size.height); + } + + // Update the matrix so that we transform the image into the orientation + // expected by the caller before drawing. + gfxMatrix matrix(OrientationMatrix(size)); + gfxContextMatrixAutoSaveRestore saveMatrix(aContext); + aContext->Multiply(matrix); + + // The region is already in the orientation expected by the caller, but we + // need it to be in the image's coordinate system, so we transform it using + // the inverse of the orientation matrix. + gfxMatrix inverseMatrix(OrientationMatrix(size, /* aInvert = */ true)); + ImageRegion region(aRegion); + region.TransformBoundsBy(inverseMatrix); + + auto orientViewport = [&](const SVGImageContext& aOldContext) { + SVGImageContext context(aOldContext); + auto oldViewport = aOldContext.GetViewportSize(); + if (oldViewport && mOrientation.SwapsWidthAndHeight()) { + // Swap width and height: + CSSIntSize newViewport(oldViewport->height, oldViewport->width); + context.SetViewportSize(Some(newViewport)); + } + return context; + }; + + return InnerImage()->Draw(aContext, size, region, aWhichFrame, + aSamplingFilter, orientViewport(aSVGContext), + aFlags, aOpacity); +} + +nsIntSize OrientedImage::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + if (!mOrientation.SwapsWidthAndHeight()) { + return InnerImage()->OptimalImageSizeForDest(aDest, aWhichFrame, + aSamplingFilter, aFlags); + } + + // Swap the size for the calculation, then swap it back for the caller. + gfxSize destSize(aDest.height, aDest.width); + nsIntSize innerImageSize(InnerImage()->OptimalImageSizeForDest( + destSize, aWhichFrame, aSamplingFilter, aFlags)); + return nsIntSize(innerImageSize.height, innerImageSize.width); +} + +NS_IMETHODIMP_(nsIntRect) +OrientedImage::GetImageSpaceInvalidationRect(const nsIntRect& aRect) { + nsIntRect rect(InnerImage()->GetImageSpaceInvalidationRect(aRect)); + + if (mOrientation.IsIdentity()) { + return rect; + } + + nsIntSize innerSize; + nsresult rv = InnerImage()->GetWidth(&innerSize.width); + rv = NS_FAILED(rv) ? rv : InnerImage()->GetHeight(&innerSize.height); + if (NS_FAILED(rv)) { + // Fall back to identity if the width and height aren't available. + return rect; + } + + // Transform the invalidation rect into the correct orientation. + gfxMatrix matrix(OrientationMatrix(innerSize)); + gfxRect invalidRect(matrix.TransformBounds( + gfxRect(rect.X(), rect.Y(), rect.Width(), rect.Height()))); + + return IntRect::RoundOut(invalidRect.X(), invalidRect.Y(), + invalidRect.Width(), invalidRect.Height()); +} + +} // namespace image +} // namespace mozilla diff --git a/image/OrientedImage.h b/image/OrientedImage.h new file mode 100644 index 0000000000..e01fcd639c --- /dev/null +++ b/image/OrientedImage.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_OrientedImage_h +#define mozilla_image_OrientedImage_h + +#include "ImageWrapper.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" +#include "Orientation.h" + +namespace mozilla { +namespace image { + +/** + * An Image wrapper that rotates and/or flips an image according to a specified + * Orientation. + * + * XXX(seth): There a known (performance, not correctness) issue with + * GetImageContainer. See the comments for that method for more information. + */ +class OrientedImage : public ImageWrapper { + typedef gfx::SourceSurface SourceSurface; + + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(OrientedImage, ImageWrapper) + + NS_IMETHOD GetWidth(int32_t* aWidth) override; + NS_IMETHOD GetHeight(int32_t* aHeight) override; + nsresult GetNativeSizes(nsTArray& aNativeSizes) override; + NS_IMETHOD GetIntrinsicSize(nsSize* aSize) override; + Maybe GetIntrinsicRatio() override; + NS_IMETHOD_(already_AddRefed) + GetFrame(uint32_t aWhichFrame, uint32_t aFlags) override; + NS_IMETHOD_(already_AddRefed) + GetFrameAtSize(const gfx::IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) override; + NS_IMETHOD_(bool) + IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) override; + NS_IMETHOD_(ImgDrawResult) + GetImageProvider(WindowRenderer* aRenderer, const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, uint32_t aFlags, + WebRenderImageProvider** aProvider) override; + NS_IMETHOD_(ImgDrawResult) + Draw(gfxContext* aContext, const nsIntSize& aSize, const ImageRegion& aRegion, + uint32_t aWhichFrame, gfx::SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) override; + NS_IMETHOD_(nsIntRect) + GetImageSpaceInvalidationRect(const nsIntRect& aRect) override; + nsIntSize OptimalImageSizeForDest(const gfxSize& aDest, uint32_t aWhichFrame, + gfx::SamplingFilter aSamplingFilter, + uint32_t aFlags) override; + + /** + * Computes a matrix that applies the rotation and reflection specified by + * aOrientation, or that matrix's inverse if aInvert is true. + * + * @param aSize The scaled size of the inner image. (When outside code + * specifies the scaled size, as with imgIContainer::Draw and its aSize + * parameter, it's necessary to swap the width and height if + * mOrientation.SwapsWidthAndHeight() is true.) + * + * @param aInvert If true, compute the inverse of the orientation matrix. + * Prefer this approach to OrientationMatrix(..).Invert(), because it's more + * numerically accurate. + */ + static gfxMatrix OrientationMatrix(Orientation aOrientation, + const nsIntSize& aSize, + bool aInvert = false); + + /** + * Returns a SourceSurface that is the result of rotating and flipping + * aSurface according to aOrientation. + */ + static already_AddRefed OrientSurface(Orientation aOrientation, + SourceSurface* aSurface); + + protected: + OrientedImage(Image* aImage, Orientation aOrientation) + : ImageWrapper(aImage), mOrientation(aOrientation) {} + + virtual ~OrientedImage() {} + + gfxMatrix OrientationMatrix(const nsIntSize& aSize, bool aInvert = false) { + return OrientationMatrix(mOrientation, aSize, aInvert); + } + + private: + Orientation mOrientation; + + friend class ImageOps; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_OrientedImage_h diff --git a/image/PlaybackType.h b/image/PlaybackType.h new file mode 100644 index 0000000000..ff820ca680 --- /dev/null +++ b/image/PlaybackType.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_PlaybackType_h +#define mozilla_image_PlaybackType_h + +#include "imgIContainer.h" + +namespace mozilla { +namespace image { + +/** + * PlaybackType identifies a surface cache entry as either a static surface or + * an animation. Note that a specific cache entry is one or the other, but + * images may be associated with both types of cache entries, since in some + * circumstances we may want to treat an animated image as if it were static. + */ +enum class PlaybackType : uint8_t { + eStatic, // Calls to DrawableRef() will always return the same surface. + eAnimated // An animation; calls to DrawableRef() may return different + // surfaces at different times. +}; + +/** + * Given an imgIContainer FRAME_* value, returns the corresponding PlaybackType + * for use in surface cache lookups. + */ +inline PlaybackType ToPlaybackType(uint32_t aWhichFrame) { + MOZ_ASSERT(aWhichFrame == imgIContainer::FRAME_FIRST || + aWhichFrame == imgIContainer::FRAME_CURRENT); + return aWhichFrame == imgIContainer::FRAME_CURRENT ? PlaybackType::eAnimated + : PlaybackType::eStatic; +} + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_PlaybackType_h diff --git a/image/ProgressTracker.cpp b/image/ProgressTracker.cpp new file mode 100644 index 0000000000..2d8720fa76 --- /dev/null +++ b/image/ProgressTracker.cpp @@ -0,0 +1,561 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" +#include "ProgressTracker.h" + +#include "imgINotificationObserver.h" +#include "imgIRequest.h" +#include "Image.h" +#include "nsNetUtil.h" +#include "nsIObserverService.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/Services.h" + +using mozilla::WeakPtr; + +namespace mozilla { +namespace image { + +static void CheckProgressConsistency(Progress aOldProgress, + Progress aNewProgress, bool aIsMultipart) { + // Check preconditions for every progress bit. + + // Error's do not get propagated from the tracker for each image part to the + // tracker for the multipart image because we don't want one bad part to + // prevent the remaining parts from showing. So we need to consider whether + // this is a tracker for a multipart image for these assertions to work. + + if (aNewProgress & FLAG_SIZE_AVAILABLE) { + // No preconditions. + } + if (aNewProgress & FLAG_DECODE_COMPLETE) { + MOZ_ASSERT(aNewProgress & FLAG_SIZE_AVAILABLE); + MOZ_ASSERT(aIsMultipart || + aNewProgress & (FLAG_FRAME_COMPLETE | FLAG_HAS_ERROR)); + } + if (aNewProgress & FLAG_FRAME_COMPLETE) { + MOZ_ASSERT(aNewProgress & FLAG_SIZE_AVAILABLE); + } + if (aNewProgress & FLAG_LOAD_COMPLETE) { + MOZ_ASSERT(aIsMultipart || + aNewProgress & (FLAG_SIZE_AVAILABLE | FLAG_HAS_ERROR)); + } + if (aNewProgress & FLAG_IS_ANIMATED) { + // No preconditions; like FLAG_HAS_TRANSPARENCY, we should normally never + // discover this *after* FLAG_SIZE_AVAILABLE, but unfortunately some corrupt + // GIFs may fool us. + } + if (aNewProgress & FLAG_HAS_TRANSPARENCY) { + // XXX We'd like to assert that transparency is only set during metadata + // decode but we don't have any way to assert that until bug 1254892 is + // fixed. + } + if (aNewProgress & FLAG_LAST_PART_COMPLETE) { + MOZ_ASSERT(aNewProgress & FLAG_LOAD_COMPLETE); + } + if (aNewProgress & FLAG_HAS_ERROR) { + // No preconditions. + } +} + +ProgressTracker::ProgressTracker() + : mMutex("ProgressTracker::mMutex"), + mImage(nullptr), + mEventTarget(WrapNotNull( + nsCOMPtr(GetMainThreadSerialEventTarget()))), + mObserversWithTargets(0), + mObservers(new ObserverTable), + mProgress(NoProgress), + mIsMultipart(false) {} + +void ProgressTracker::SetImage(Image* aImage) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(aImage, "Setting null image"); + MOZ_ASSERT(!mImage, "Setting image when we already have one"); + mImage = aImage; +} + +void ProgressTracker::ResetImage() { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mImage, "Resetting image when it's already null!"); + mImage = nullptr; +} + +uint32_t ProgressTracker::GetImageStatus() const { + uint32_t status = imgIRequest::STATUS_NONE; + + // Translate our current state to a set of imgIRequest::STATE_* flags. + if (mProgress & FLAG_SIZE_AVAILABLE) { + status |= imgIRequest::STATUS_SIZE_AVAILABLE; + } + if (mProgress & FLAG_DECODE_COMPLETE) { + status |= imgIRequest::STATUS_DECODE_COMPLETE; + } + if (mProgress & FLAG_FRAME_COMPLETE) { + status |= imgIRequest::STATUS_FRAME_COMPLETE; + } + if (mProgress & FLAG_LOAD_COMPLETE) { + status |= imgIRequest::STATUS_LOAD_COMPLETE; + } + if (mProgress & FLAG_IS_ANIMATED) { + status |= imgIRequest::STATUS_IS_ANIMATED; + } + if (mProgress & FLAG_HAS_TRANSPARENCY) { + status |= imgIRequest::STATUS_HAS_TRANSPARENCY; + } + if (mProgress & FLAG_HAS_ERROR) { + status |= imgIRequest::STATUS_ERROR; + } + + return status; +} + +// A helper class to allow us to call SyncNotify asynchronously. +class AsyncNotifyRunnable : public Runnable { + public: + AsyncNotifyRunnable(ProgressTracker* aTracker, IProgressObserver* aObserver) + : Runnable("ProgressTracker::AsyncNotifyRunnable"), mTracker(aTracker) { + MOZ_ASSERT(NS_IsMainThread(), "Should be created on the main thread"); + MOZ_ASSERT(aTracker, "aTracker should not be null"); + MOZ_ASSERT(aObserver, "aObserver should not be null"); + mObservers.AppendElement(aObserver); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "Should be running on the main thread"); + MOZ_ASSERT(mTracker, "mTracker should not be null"); + for (uint32_t i = 0; i < mObservers.Length(); ++i) { + mObservers[i]->ClearPendingNotify(); + mTracker->SyncNotify(mObservers[i]); + } + + mTracker->mRunnable = nullptr; + return NS_OK; + } + + void AddObserver(IProgressObserver* aObserver) { + mObservers.AppendElement(aObserver); + } + + void RemoveObserver(IProgressObserver* aObserver) { + mObservers.RemoveElement(aObserver); + } + + private: + friend class ProgressTracker; + + RefPtr mTracker; + nsTArray> mObservers; +}; + +ProgressTracker::RenderBlockingRunnable::RenderBlockingRunnable( + already_AddRefed&& aEvent) + : PrioritizableRunnable(std::move(aEvent), + nsIRunnablePriority::PRIORITY_RENDER_BLOCKING) {} + +void ProgressTracker::RenderBlockingRunnable::AddObserver( + IProgressObserver* aObserver) { + static_cast(mRunnable.get())->AddObserver(aObserver); +} + +void ProgressTracker::RenderBlockingRunnable::RemoveObserver( + IProgressObserver* aObserver) { + static_cast(mRunnable.get())->RemoveObserver(aObserver); +} + +/* static */ +already_AddRefed +ProgressTracker::RenderBlockingRunnable::Create( + already_AddRefed&& aEvent) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr event( + new ProgressTracker::RenderBlockingRunnable(std::move(aEvent))); + return event.forget(); +} + +void ProgressTracker::Notify(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aObserver->NotificationsDeferred()) { + // There is a pending notification, or the observer isn't ready yet. + return; + } + + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + RefPtr image = GetImage(); + LOG_FUNC_WITH_PARAM(gImgLog, "ProgressTracker::Notify async", "uri", image); + } + + aObserver->MarkPendingNotify(); + + // If we have an existing runnable that we can use, we just append this + // observer to its list of observers to be notified. This ensures we don't + // unnecessarily delay onload. + if (mRunnable) { + mRunnable->AddObserver(aObserver); + } else if (!AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + // Avoid dispatch if we are late in shutdown. + RefPtr ev = new AsyncNotifyRunnable(this, aObserver); + mRunnable = ProgressTracker::RenderBlockingRunnable::Create(ev.forget()); + mEventTarget->Dispatch(mRunnable, NS_DISPATCH_NORMAL); + } +} + +// A helper class to allow us to call SyncNotify asynchronously for a given, +// fixed, state. +class AsyncNotifyCurrentStateRunnable : public Runnable { + public: + AsyncNotifyCurrentStateRunnable(ProgressTracker* aProgressTracker, + IProgressObserver* aObserver) + : Runnable("image::AsyncNotifyCurrentStateRunnable"), + mProgressTracker(aProgressTracker), + mObserver(aObserver) { + MOZ_ASSERT(NS_IsMainThread(), "Should be created on the main thread"); + MOZ_ASSERT(mProgressTracker, "mProgressTracker should not be null"); + MOZ_ASSERT(mObserver, "mObserver should not be null"); + mImage = mProgressTracker->GetImage(); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "Should be running on the main thread"); + mObserver->ClearPendingNotify(); + + mProgressTracker->SyncNotify(mObserver); + return NS_OK; + } + + private: + RefPtr mProgressTracker; + RefPtr mObserver; + + // We have to hold on to a reference to the tracker's image, just in case + // it goes away while we're in the event queue. + RefPtr mImage; +}; + +void ProgressTracker::NotifyCurrentState(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aObserver->NotificationsDeferred()) { + // There is a pending notification, or the observer isn't ready yet. + return; + } + + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + RefPtr image = GetImage(); + LOG_FUNC_WITH_PARAM(gImgLog, "ProgressTracker::NotifyCurrentState", "uri", + image); + } + + aObserver->MarkPendingNotify(); + + // Avoid dispatch if we are late in shutdown. + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + nsCOMPtr ev = + new AsyncNotifyCurrentStateRunnable(this, aObserver); + mEventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL); + } +} + +/** + * ImageObserverNotifier is a helper type that abstracts over the difference + * between sending notifications to all of the observers in an ObserverTable, + * and sending them to a single observer. This allows the same notification code + * to be used for both cases. + */ +template +struct ImageObserverNotifier; + +template <> +struct MOZ_STACK_CLASS ImageObserverNotifier { + explicit ImageObserverNotifier(const ObserverTable* aObservers, + bool aIgnoreDeferral = false) + : mObservers(aObservers), mIgnoreDeferral(aIgnoreDeferral) {} + + template + void operator()(Lambda aFunc) { + for (const auto& weakObserver : mObservers->Values()) { + RefPtr observer = weakObserver.get(); + if (observer && (mIgnoreDeferral || !observer->NotificationsDeferred())) { + aFunc(observer); + } + } + } + + private: + const ObserverTable* mObservers; + const bool mIgnoreDeferral; +}; + +template <> +struct MOZ_STACK_CLASS ImageObserverNotifier { + explicit ImageObserverNotifier(IProgressObserver* aObserver) + : mObserver(aObserver) {} + + template + void operator()(Lambda aFunc) { + if (mObserver && !mObserver->NotificationsDeferred()) { + aFunc(mObserver); + } + } + + private: + IProgressObserver* mObserver; +}; + +template +void SyncNotifyInternal(const T& aObservers, bool aHasImage, Progress aProgress, + const nsIntRect& aDirtyRect) { + MOZ_ASSERT(NS_IsMainThread()); + + typedef imgINotificationObserver I; + ImageObserverNotifier notify(aObservers); + + if (aProgress & FLAG_SIZE_AVAILABLE) { + notify([](IProgressObserver* aObs) { aObs->Notify(I::SIZE_AVAILABLE); }); + } + + if (aHasImage) { + // OnFrameUpdate + // If there's any content in this frame at all (always true for + // vector images, true for raster images that have decoded at + // least one frame) then send OnFrameUpdate. + if (!aDirtyRect.IsEmpty()) { + notify([&](IProgressObserver* aObs) { + aObs->Notify(I::FRAME_UPDATE, &aDirtyRect); + }); + } + + if (aProgress & FLAG_FRAME_COMPLETE) { + notify([](IProgressObserver* aObs) { aObs->Notify(I::FRAME_COMPLETE); }); + } + + if (aProgress & FLAG_HAS_TRANSPARENCY) { + notify( + [](IProgressObserver* aObs) { aObs->Notify(I::HAS_TRANSPARENCY); }); + } + + if (aProgress & FLAG_IS_ANIMATED) { + notify([](IProgressObserver* aObs) { aObs->Notify(I::IS_ANIMATED); }); + } + } + + if (aProgress & FLAG_DECODE_COMPLETE) { + MOZ_ASSERT(aHasImage, "Stopped decoding without ever having an image?"); + notify([](IProgressObserver* aObs) { aObs->Notify(I::DECODE_COMPLETE); }); + } + + if (aProgress & FLAG_LOAD_COMPLETE) { + notify([=](IProgressObserver* aObs) { + aObs->OnLoadComplete(aProgress & FLAG_LAST_PART_COMPLETE); + }); + } +} + +void ProgressTracker::SyncNotifyProgress(Progress aProgress, + const nsIntRect& aInvalidRect + /* = nsIntRect() */) { + MOZ_ASSERT(NS_IsMainThread(), "Use mObservers on main thread only"); + + Progress progress = Difference(aProgress); + CheckProgressConsistency(mProgress, mProgress | progress, mIsMultipart); + + // Apply the changes. + mProgress |= progress; + + // Send notifications. + mObservers.Read([&](const ObserverTable* aTable) { + SyncNotifyInternal(aTable, HasImage(), progress, aInvalidRect); + }); + + if (progress & FLAG_HAS_ERROR) { + FireFailureNotification(); + } +} + +void ProgressTracker::SyncNotify(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr image = GetImage(); + LOG_SCOPE_WITH_PARAM(gImgLog, "ProgressTracker::SyncNotify", "uri", image); + + nsIntRect rect; + if (image) { + int32_t width, height; + if (NS_FAILED(image->GetWidth(&width)) || + NS_FAILED(image->GetHeight(&height))) { + // Either the image has no intrinsic size, or it has an error. + rect = GetMaxSizedIntRect(); + } else { + rect.SizeTo(width, height); + } + } + + SyncNotifyInternal(aObserver, !!image, mProgress, rect); +} + +void ProgressTracker::EmulateRequestFinished(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread(), + "SyncNotifyState and mObservers are not threadsafe"); + RefPtr kungFuDeathGrip(aObserver); + + if (!(mProgress & FLAG_LOAD_COMPLETE)) { + aObserver->OnLoadComplete(true); + } +} + +already_AddRefed ProgressTracker::GetEventTarget() const { + MutexAutoLock lock(mMutex); + nsCOMPtr target = mEventTarget; + return target.forget(); +} + +void ProgressTracker::AddObserver(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr observer = aObserver; + + nsCOMPtr target = observer->GetEventTarget(); + if (target) { + if (mObserversWithTargets == 0) { + // On the first observer with a target (i.e. listener), always accept its + // event target; this may be for a specific DocGroup, or it may be the + // unlabelled main thread target. + MutexAutoLock lock(mMutex); + mEventTarget = WrapNotNull(target); + } else if (mEventTarget.get() != target.get()) { + // If a subsequent observer comes in with a different target, we need to + // switch to use the unlabelled main thread target, if we haven't already. + MutexAutoLock lock(mMutex); + nsCOMPtr mainTarget(do_GetMainThread()); + mEventTarget = WrapNotNull(mainTarget); + } + ++mObserversWithTargets; + } + + mObservers.Write([=](ObserverTable* aTable) { + MOZ_ASSERT(!aTable->Contains(observer), + "Adding duplicate entry for image observer"); + + WeakPtr weakPtr = observer.get(); + aTable->InsertOrUpdate(observer, weakPtr); + }); + + MOZ_ASSERT(mObserversWithTargets <= ObserverCount()); +} + +bool ProgressTracker::RemoveObserver(IProgressObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr observer = aObserver; + + // Remove the observer from the list. + bool removed = mObservers.Write( + [observer](ObserverTable* aTable) { return aTable->Remove(observer); }); + + // Sometimes once an image is decoded, and all of its observers removed, a new + // document may request the same image. Thus we need to clear our event target + // state when the last observer is removed, so that we select the most + // appropriate event target when a new observer is added. Since the event + // target may have changed (e.g. due to the scheduler group going away before + // we were removed), so we should be cautious comparing this target against + // anything at this stage. + if (removed) { + nsCOMPtr target = observer->GetEventTarget(); + if (target) { + MOZ_ASSERT(mObserversWithTargets > 0); + --mObserversWithTargets; + + // If we're shutting down there's no need to update event targets. + if ((mObserversWithTargets == 0) && + !AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + MutexAutoLock lock(mMutex); + nsCOMPtr target(do_GetMainThread()); + mEventTarget = WrapNotNull(target); + } + } + + MOZ_ASSERT(mObserversWithTargets <= ObserverCount()); + } + + // Observers can get confused if they don't get all the proper teardown + // notifications. Part ways on good terms. + if (removed && !aObserver->NotificationsDeferred()) { + EmulateRequestFinished(aObserver); + } + + // Make sure we don't give callbacks to an observer that isn't interested in + // them any more. + if (aObserver->NotificationsDeferred() && mRunnable) { + mRunnable->RemoveObserver(aObserver); + aObserver->ClearPendingNotify(); + } + + return removed; +} + +uint32_t ProgressTracker::ObserverCount() const { + MOZ_ASSERT(NS_IsMainThread()); + return mObservers.Read( + [](const ObserverTable* aTable) { return aTable->Count(); }); +} + +void ProgressTracker::OnUnlockedDraw() { + MOZ_ASSERT(NS_IsMainThread()); + mObservers.Read([](const ObserverTable* aTable) { + ImageObserverNotifier notify(aTable); + notify([](IProgressObserver* aObs) { + aObs->Notify(imgINotificationObserver::UNLOCKED_DRAW); + }); + }); +} + +void ProgressTracker::ResetForNewRequest() { + MOZ_ASSERT(NS_IsMainThread()); + mProgress = NoProgress; +} + +void ProgressTracker::OnDiscard() { + MOZ_ASSERT(NS_IsMainThread()); + mObservers.Read([](const ObserverTable* aTable) { + ImageObserverNotifier notify(aTable); + notify([](IProgressObserver* aObs) { + aObs->Notify(imgINotificationObserver::DISCARD); + }); + }); +} + +void ProgressTracker::OnImageAvailable() { + MOZ_ASSERT(NS_IsMainThread()); + // Notify any imgRequestProxys that are observing us that we have an Image. + mObservers.Read([](const ObserverTable* aTable) { + ImageObserverNotifier notify( + aTable, /* aIgnoreDeferral = */ true); + notify([](IProgressObserver* aObs) { aObs->SetHasImage(); }); + }); +} + +void ProgressTracker::FireFailureNotification() { + MOZ_ASSERT(NS_IsMainThread()); + + // Some kind of problem has happened with image decoding. + // Report the URI to net:failed-to-process-uri-conent observers. + RefPtr image = GetImage(); + if (image) { + // Should be on main thread, so ok to create a new nsIURI. + nsCOMPtr uri = image->GetURI(); + if (uri) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(uri, "net:failed-to-process-uri-content", nullptr); + } + } + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/ProgressTracker.h b/image/ProgressTracker.h new file mode 100644 index 0000000000..568fb5e28c --- /dev/null +++ b/image/ProgressTracker.h @@ -0,0 +1,264 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ProgressTracker_h +#define mozilla_image_ProgressTracker_h + +#include "CopyOnWrite.h" +#include "mozilla/NotNull.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WeakPtr.h" +#include "nsTHashMap.h" +#include "nsCOMPtr.h" +#include "nsTObserverArray.h" +#include "nsThreadUtils.h" +#include "nsRect.h" +#include "IProgressObserver.h" + +class nsIRunnable; + +namespace mozilla { +namespace image { + +class AsyncNotifyRunnable; +class AsyncNotifyCurrentStateRunnable; +class Image; + +/** + * Image progress bitflags. + * + * See CheckProgressConsistency() for the invariants we enforce about the + * ordering dependencies between these flags. + */ +enum { + FLAG_SIZE_AVAILABLE = 1u << 0, // STATUS_SIZE_AVAILABLE + FLAG_DECODE_COMPLETE = 1u << 1, // STATUS_DECODE_COMPLETE + FLAG_FRAME_COMPLETE = 1u << 2, // STATUS_FRAME_COMPLETE + FLAG_LOAD_COMPLETE = 1u << 3, // STATUS_LOAD_COMPLETE + FLAG_IS_ANIMATED = 1u << 6, // STATUS_IS_ANIMATED + FLAG_HAS_TRANSPARENCY = 1u << 7, // STATUS_HAS_TRANSPARENCY + FLAG_LAST_PART_COMPLETE = 1u << 8, + FLAG_HAS_ERROR = 1u << 9 // STATUS_ERROR +}; + +typedef uint32_t Progress; + +const uint32_t NoProgress = 0; + +inline Progress LoadCompleteProgress(bool aLastPart, bool aError, + nsresult aStatus) { + Progress progress = FLAG_LOAD_COMPLETE; + if (aLastPart) { + progress |= FLAG_LAST_PART_COMPLETE; + } + if (NS_FAILED(aStatus) || aError) { + progress |= FLAG_HAS_ERROR; + } + return progress; +} + +/** + * ProgressTracker stores its observers in an ObserverTable, which is a hash + * table mapping raw pointers to WeakPtr's to the same objects. This sounds like + * unnecessary duplication of information, but it's necessary for stable hash + * values since WeakPtr's lose the knowledge of which object they used to point + * to when that object is destroyed. + * + * ObserverTable subclasses nsTHashMap to add reference counting + * support and a copy constructor, both of which are needed for use with + * CopyOnWrite. + */ +class ObserverTable : public nsTHashMap, + WeakPtr> { + public: + NS_INLINE_DECL_REFCOUNTING(ObserverTable); + + ObserverTable() = default; + + ObserverTable(const ObserverTable& aOther) + : nsTHashMap, WeakPtr>( + aOther.Clone()) { + NS_WARNING("Forced to copy ObserverTable due to nested notifications"); + } + + private: + ~ObserverTable() {} +}; + +/** + * ProgressTracker is a class that records an Image's progress through the + * loading and decoding process, and makes it possible to send notifications to + * IProgressObservers, both synchronously and asynchronously. + * + * When a new observer needs to be notified of the current progress of an image, + * call the Notify() method on this class with the relevant observer as its + * argument, and the notifications will be replayed to the observer + * asynchronously. + */ +class ProgressTracker : public mozilla::SupportsWeakPtr { + virtual ~ProgressTracker() {} + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ProgressTracker) + + ProgressTracker(); + + bool HasImage() const { + MutexAutoLock lock(mMutex); + return mImage; + } + already_AddRefed GetImage() const { + MutexAutoLock lock(mMutex); + RefPtr image = mImage; + return image.forget(); + } + + // Get the current image status (as in imgIRequest). + uint32_t GetImageStatus() const; + + // Get the current Progress. + Progress GetProgress() const { return mProgress; } + + // Schedule an asynchronous "replaying" of all the notifications that would + // have to happen to put us in the current state. + // We will also take note of any notifications that happen between the time + // Notify() is called and when we call SyncNotify on |aObserver|, and replay + // them as well. + // Should be called on the main thread only, since observers and GetURI are + // not threadsafe. + void Notify(IProgressObserver* aObserver); + + // Schedule an asynchronous "replaying" of all the notifications that would + // have to happen to put us in the state we are in right now. + // Unlike Notify(), does *not* take into account future notifications. + // This is only useful if you do not have an imgRequest, e.g., if you are a + // static request returned from imgIRequest::GetStaticRequest(). + // Should be called on the main thread only, since observers and GetURI are + // not threadsafe. + void NotifyCurrentState(IProgressObserver* aObserver); + + // "Replay" all of the notifications that would have to happen to put us in + // the state we're currently in. + // Only use this if you're already servicing an asynchronous call (e.g. + // OnStartRequest). + // Should be called on the main thread only, since observers and GetURI are + // not threadsafe. + void SyncNotify(IProgressObserver* aObserver); + + // Get this ProgressTracker ready for a new request. This resets all the + // state that doesn't persist between requests. + void ResetForNewRequest(); + + // Stateless notifications. These are dispatched and immediately forgotten + // about. All of these notifications are main thread only. + void OnDiscard(); + void OnUnlockedDraw(); + void OnImageAvailable(); + + // Compute the difference between this our progress and aProgress. This allows + // callers to predict whether SyncNotifyProgress will send any notifications. + Progress Difference(Progress aProgress) const { + return ~mProgress & aProgress; + } + + // Update our state to incorporate the changes in aProgress and synchronously + // notify our observers. + // + // Because this may result in recursive notifications, no decoding locks may + // be held. Called on the main thread only. + void SyncNotifyProgress(Progress aProgress, + const nsIntRect& aInvalidRect = nsIntRect()); + + // We manage a set of observers that are using an image and thus concerned + // with its loading progress. Weak pointers. + void AddObserver(IProgressObserver* aObserver); + bool RemoveObserver(IProgressObserver* aObserver); + uint32_t ObserverCount() const; + + // Get the event target we should currently dispatch events to. + already_AddRefed GetEventTarget() const; + + // Resets our weak reference to our image. Image subclasses should call this + // in their destructor. + void ResetImage(); + + // Tell this progress tracker that it is for a multipart image. + void SetIsMultipart() { mIsMultipart = true; } + + private: + friend class AsyncNotifyRunnable; + friend class AsyncNotifyCurrentStateRunnable; + friend class ImageFactory; + + ProgressTracker(const ProgressTracker& aOther) = delete; + + // Sets our weak reference to our image. Only ImageFactory should call this. + void SetImage(Image* aImage); + + // Send some notifications that would be necessary to make |aObserver| believe + // the request is finished downloading and decoding. We only send + // FLAG_LOAD_COMPLETE and FLAG_ONLOAD_UNBLOCKED, and only if necessary. + void EmulateRequestFinished(IProgressObserver* aObserver); + + // Main thread only because it deals with the observer service. + void FireFailureNotification(); + + // Wrapper for AsyncNotifyRunnable to make it have medium high priority like + // other imagelib runnables. + class RenderBlockingRunnable final : public PrioritizableRunnable { + explicit RenderBlockingRunnable( + already_AddRefed&& aEvent); + virtual ~RenderBlockingRunnable() = default; + + public: + void AddObserver(IProgressObserver* aObserver); + void RemoveObserver(IProgressObserver* aObserver); + + static already_AddRefed Create( + already_AddRefed&& aEvent); + }; + + // The runnable, if any, that we've scheduled to deliver async notifications. + RefPtr mRunnable; + + // mMutex protects access to mImage and mEventTarget. + mutable Mutex mMutex MOZ_UNANNOTATED; + + // mImage is a weak ref; it should be set to null when the image goes out of + // scope. + Image* mImage; + + // mEventTarget is the current, best effort event target to dispatch + // notifications to from the decoder threads. It will change as observers are + // added and removed (see mObserversWithTargets). + NotNull> mEventTarget; + + // How many observers have been added that have an explicit event target. + // When the first observer is added with an explicit event target, we will + // default to that as long as all observers use the same target. If a new + // observer is added which has a different event target, we will switch to + // using the unlabeled main thread event target which is safe for all + // observers. If all observers with explicit event targets are removed, we + // will revert back to the initial event target (for SystemGroup). An + // observer without an explicit event target does not care what context it + // is dispatched in, and thus does not impact the state. + uint32_t mObserversWithTargets; + + // Hashtable of observers attached to the image. Each observer represents a + // consumer using the image. Main thread only. + CopyOnWrite mObservers; + + Progress mProgress; + + // Whether this is a progress tracker for a multipart image. + bool mIsMultipart; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ProgressTracker_h diff --git a/image/RasterImage.cpp b/image/RasterImage.cpp new file mode 100644 index 0000000000..775238b460 --- /dev/null +++ b/image/RasterImage.cpp @@ -0,0 +1,1785 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Must #include ImageLogging.h before any IPDL-generated files or other files +// that #include prlog.h +#include "RasterImage.h" + +#include + +#include +#include + +#include "DecodePool.h" +#include "Decoder.h" +#include "FrameAnimator.h" +#include "GeckoProfiler.h" +#include "IDecodingTask.h" +#include "ImageLogging.h" +#include "ImageRegion.h" +#include "LookupResult.h" +#include "OrientedImage.h" +#include "SourceBuffer.h" +#include "SurfaceCache.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SizeOfState.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Scale.h" +#include "nsComponentManagerUtils.h" +#include "nsError.h" +#include "nsIConsoleService.h" +#include "nsIInputStream.h" +#include "nsIScriptError.h" +#include "nsISupportsPrimitives.h" +#include "nsMemory.h" +#include "nsPresContext.h" +#include "nsProperties.h" +#include "prenv.h" +#include "prsystem.h" +#include "WindowRenderer.h" + +namespace mozilla { + +using namespace gfx; +using namespace layers; + +namespace image { + +using std::ceil; +using std::min; + +#ifndef DEBUG +NS_IMPL_ISUPPORTS(RasterImage, imgIContainer) +#else +NS_IMPL_ISUPPORTS(RasterImage, imgIContainer, imgIContainerDebug) +#endif + +//****************************************************************************** +RasterImage::RasterImage(nsIURI* aURI /* = nullptr */) + : ImageResource(aURI), // invoke superclass's constructor + mSize(0, 0), + mLockCount(0), + mDecoderType(DecoderType::UNKNOWN), + mDecodeCount(0), +#ifdef DEBUG + mFramesNotified(0), +#endif + mSourceBuffer(MakeNotNull()) { +} + +//****************************************************************************** +RasterImage::~RasterImage() { + // Make sure our SourceBuffer is marked as complete. This will ensure that any + // outstanding decoders terminate. + if (!mSourceBuffer->IsComplete()) { + mSourceBuffer->Complete(NS_ERROR_ABORT); + } + + // Release all frames from the surface cache. + SurfaceCache::RemoveImage(ImageKey(this)); + + // Record Telemetry. + Telemetry::Accumulate(Telemetry::IMAGE_DECODE_COUNT, mDecodeCount); +} + +nsresult RasterImage::Init(const char* aMimeType, uint32_t aFlags) { + // We don't support re-initialization + if (mInitialized) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Not sure an error can happen before init, but be safe + if (mError) { + return NS_ERROR_FAILURE; + } + + // We want to avoid redecodes for transient images. + MOZ_ASSERT_IF(aFlags & INIT_FLAG_TRANSIENT, + !(aFlags & INIT_FLAG_DISCARDABLE)); + + // Store initialization data + StoreDiscardable(!!(aFlags & INIT_FLAG_DISCARDABLE)); + StoreWantFullDecode(!!(aFlags & INIT_FLAG_DECODE_IMMEDIATELY)); + StoreTransient(!!(aFlags & INIT_FLAG_TRANSIENT)); + StoreSyncLoad(!!(aFlags & INIT_FLAG_SYNC_LOAD)); + + // Use the MIME type to select a decoder type, and make sure there *is* a + // decoder for this MIME type. + NS_ENSURE_ARG_POINTER(aMimeType); + mDecoderType = DecoderFactory::GetDecoderType(aMimeType); + if (mDecoderType == DecoderType::UNKNOWN) { + return NS_ERROR_FAILURE; + } + + // Lock this image's surfaces in the SurfaceCache if we're not discardable. + if (!LoadDiscardable()) { + mLockCount++; + SurfaceCache::LockImage(ImageKey(this)); + } + + // Set the default flags according to the decoder type to allow preferences to + // be stored if necessary. + mDefaultDecoderFlags = + DecoderFactory::GetDefaultDecoderFlagsForType(mDecoderType); + + // Mark us as initialized + mInitialized = true; + + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP_(void) +RasterImage::RequestRefresh(const TimeStamp& aTime) { + if (HadRecentRefresh(aTime)) { + return; + } + + EvaluateAnimation(); + + if (!mAnimating) { + return; + } + + RefreshResult res; + if (mAnimationState) { + MOZ_ASSERT(mFrameAnimator); + res = mFrameAnimator->RequestRefresh(*mAnimationState, aTime); + } + +#ifdef DEBUG + if (res.mFrameAdvanced) { + mFramesNotified++; + } +#endif + + // Notify listeners that our frame has actually changed, but do this only + // once for all frames that we've now passed (if AdvanceFrame() was called + // more than once). + if (!res.mDirtyRect.IsEmpty() || res.mFrameAdvanced) { + auto dirtyRect = OrientedIntRect::FromUnknownRect(res.mDirtyRect); + NotifyProgress(NoProgress, dirtyRect); + } + + if (res.mAnimationFinished) { + StoreAnimationFinished(true); + EvaluateAnimation(); + } +} + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::GetWidth(int32_t* aWidth) { + NS_ENSURE_ARG_POINTER(aWidth); + + if (mError) { + *aWidth = 0; + return NS_ERROR_FAILURE; + } + + *aWidth = mSize.width; + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::GetHeight(int32_t* aHeight) { + NS_ENSURE_ARG_POINTER(aHeight); + + if (mError) { + *aHeight = 0; + return NS_ERROR_FAILURE; + } + + *aHeight = mSize.height; + return NS_OK; +} + +//****************************************************************************** +void RasterImage::MediaFeatureValuesChangedAllDocuments( + const mozilla::MediaFeatureChange& aChange) {} + +//****************************************************************************** +nsresult RasterImage::GetNativeSizes(nsTArray& aNativeSizes) { + if (mError) { + return NS_ERROR_FAILURE; + } + + aNativeSizes.Clear(); + + if (mNativeSizes.IsEmpty()) { + aNativeSizes.AppendElement(mSize.ToUnknownSize()); + } else { + for (const auto& size : mNativeSizes) { + aNativeSizes.AppendElement(size.ToUnknownSize()); + } + } + + return NS_OK; +} + +//****************************************************************************** +size_t RasterImage::GetNativeSizesLength() { + if (mError || !LoadHasSize()) { + return 0; + } + + if (mNativeSizes.IsEmpty()) { + return 1; + } + + return mNativeSizes.Length(); +} + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::GetIntrinsicSize(nsSize* aSize) { + if (mError) { + return NS_ERROR_FAILURE; + } + + *aSize = nsSize(nsPresContext::CSSPixelsToAppUnits(mSize.width), + nsPresContext::CSSPixelsToAppUnits(mSize.height)); + return NS_OK; +} + +//****************************************************************************** +Maybe RasterImage::GetIntrinsicRatio() { + if (mError) { + return Nothing(); + } + + return Some(AspectRatio::FromSize(mSize.width, mSize.height)); +} + +NS_IMETHODIMP_(Orientation) +RasterImage::GetOrientation() { return mOrientation; } + +NS_IMETHODIMP_(Resolution) +RasterImage::GetResolution() { return mResolution; } + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::GetType(uint16_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = imgIContainer::TYPE_RASTER; + return NS_OK; +} + +NS_IMETHODIMP +RasterImage::GetProviderId(uint32_t* aId) { + NS_ENSURE_ARG_POINTER(aId); + + *aId = ImageResource::GetImageProviderId(); + return NS_OK; +} + +LookupResult RasterImage::LookupFrameInternal(const OrientedIntSize& aSize, + uint32_t aFlags, + PlaybackType aPlaybackType, + bool aMarkUsed) { + if (mAnimationState && aPlaybackType == PlaybackType::eAnimated) { + MOZ_ASSERT(mFrameAnimator); + MOZ_ASSERT(ToSurfaceFlags(aFlags) == DefaultSurfaceFlags(), + "Can't composite frames with non-default surface flags"); + return mFrameAnimator->GetCompositedFrame(*mAnimationState, aMarkUsed); + } + + SurfaceFlags surfaceFlags = ToSurfaceFlags(aFlags); + + // We don't want any substitution for sync decodes, and substitution would be + // illegal when high quality downscaling is disabled, so we use + // SurfaceCache::Lookup in this case. + if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) { + return SurfaceCache::Lookup( + ImageKey(this), + RasterSurfaceKey(aSize.ToUnknownSize(), surfaceFlags, + PlaybackType::eStatic), + aMarkUsed); + } + + // We'll return the best match we can find to the requested frame. + return SurfaceCache::LookupBestMatch( + ImageKey(this), + RasterSurfaceKey(aSize.ToUnknownSize(), surfaceFlags, + PlaybackType::eStatic), + aMarkUsed); +} + +LookupResult RasterImage::LookupFrame(const OrientedIntSize& aSize, + uint32_t aFlags, + PlaybackType aPlaybackType, + bool aMarkUsed) { + MOZ_ASSERT(NS_IsMainThread()); + + // If we're opaque, we don't need to care about premultiplied alpha, because + // that can only matter for frames with transparency. + if (IsOpaque()) { + aFlags &= ~FLAG_DECODE_NO_PREMULTIPLY_ALPHA; + } + + OrientedIntSize requestedSize = + CanDownscaleDuringDecode(aSize, aFlags) ? aSize : mSize; + if (requestedSize.IsEmpty()) { + // Can't decode to a surface of zero size. + return LookupResult(MatchType::NOT_FOUND); + } + + LookupResult result = + LookupFrameInternal(requestedSize, aFlags, aPlaybackType, aMarkUsed); + + if (!result && !LoadHasSize()) { + // We can't request a decode without knowing our intrinsic size. Give up. + return LookupResult(MatchType::NOT_FOUND); + } + + // We want to trigger a decode if and only if: + // 1) There is no pending decode + // 2) There is no acceptable size decoded + // 3) The pending decode has not produced a frame yet, a sync decode is + // requested, and we have all the source data. Without the source data, we + // will just trigger another async decode anyways. + // + // TODO(aosmond): We should better handle case 3. We should actually return + // TEMPORARY_ERROR or NOT_READY if we don't have all the source data and a + // sync decode is requested. If there is a pending decode and we have all the + // source data, we should always be able to block on the frame's monitor -- + // perhaps this could be accomplished by preallocating the first frame buffer + // when we create the decoder. + const bool syncDecode = aFlags & FLAG_SYNC_DECODE; + const bool avoidRedecode = aFlags & FLAG_AVOID_REDECODE_FOR_SIZE; + if (result.Type() == MatchType::NOT_FOUND || + (result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND && + !avoidRedecode) || + (syncDecode && !avoidRedecode && !result && LoadAllSourceData())) { + // We don't have a copy of this frame, and there's no decoder working on + // one. (Or we're sync decoding and the existing decoder hasn't even started + // yet.) Trigger decoding so it'll be available next time. + MOZ_ASSERT(aPlaybackType != PlaybackType::eAnimated || + StaticPrefs::image_mem_animated_discardable_AtStartup() || + !mAnimationState || mAnimationState->KnownFrameCount() < 1, + "Animated frames should be locked"); + + // The surface cache may suggest the preferred size we are supposed to + // decode at. This should only happen if we accept substitutions. + if (!result.SuggestedSize().IsEmpty()) { + MOZ_ASSERT(!syncDecode && (aFlags & FLAG_HIGH_QUALITY_SCALING)); + requestedSize = OrientedIntSize::FromUnknownSize(result.SuggestedSize()); + } + + bool ranSync = false, failed = false; + Decode(requestedSize, aFlags, aPlaybackType, ranSync, failed); + if (failed) { + result.SetFailedToRequestDecode(); + } + + // If we can or did sync decode, we should already have the frame. + if (ranSync || syncDecode) { + result = + LookupFrameInternal(requestedSize, aFlags, aPlaybackType, aMarkUsed); + } + } + + if (!result) { + // We still weren't able to get a frame. Give up. + return result; + } + + // Sync decoding guarantees that we got the frame, but if it's owned by an + // async decoder that's currently running, the contents of the frame may not + // be available yet. Make sure we get everything. + if (LoadAllSourceData() && syncDecode) { + result.Surface()->WaitUntilFinished(); + } + + // If we could have done some decoding in this function we need to check if + // that decoding encountered an error and hence aborted the surface. We want + // to avoid calling IsAborted if we weren't passed any sync decode flag + // because IsAborted acquires the monitor for the imgFrame. + if (aFlags & (FLAG_SYNC_DECODE | FLAG_SYNC_DECODE_IF_FAST) && + result.Surface()->IsAborted()) { + DrawableSurface tmp = std::move(result.Surface()); + return result; + } + + return result; +} + +bool RasterImage::IsOpaque() { + if (mError) { + return false; + } + + Progress progress = mProgressTracker->GetProgress(); + + // If we haven't yet finished decoding, the safe answer is "not opaque". + if (!(progress & FLAG_DECODE_COMPLETE)) { + return false; + } + + // Other, we're opaque if FLAG_HAS_TRANSPARENCY is not set. + return !(progress & FLAG_HAS_TRANSPARENCY); +} + +NS_IMETHODIMP_(bool) +RasterImage::WillDrawOpaqueNow() { + if (!IsOpaque()) { + return false; + } + + if (mAnimationState) { + if (!StaticPrefs::image_mem_animated_discardable_AtStartup()) { + // We never discard frames of animated images. + return true; + } else { + if (mAnimationState->GetCompositedFrameInvalid()) { + // We're not going to draw anything at all. + return false; + } + } + } + + // If we are not locked our decoded data could get discard at any time (ie + // between the call to this function and when we are asked to draw), so we + // have to return false if we are unlocked. + if (mLockCount == 0) { + return false; + } + + LookupResult result = SurfaceCache::LookupBestMatch( + ImageKey(this), + RasterSurfaceKey(mSize.ToUnknownSize(), DefaultSurfaceFlags(), + PlaybackType::eStatic), + /* aMarkUsed = */ false); + MatchType matchType = result.Type(); + if (matchType == MatchType::NOT_FOUND || matchType == MatchType::PENDING || + !result.Surface()->IsFinished()) { + return false; + } + + return true; +} + +void RasterImage::OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) { + MOZ_ASSERT(mProgressTracker); + + bool animatedFramesDiscarded = + mAnimationState && aSurfaceKey.Playback() == PlaybackType::eAnimated; + + nsCOMPtr eventTarget; + if (mProgressTracker) { + eventTarget = mProgressTracker->GetEventTarget(); + } else { + eventTarget = do_GetMainThread(); + } + + RefPtr image = this; + nsCOMPtr ev = + NS_NewRunnableFunction("RasterImage::OnSurfaceDiscarded", [=]() -> void { + image->OnSurfaceDiscardedInternal(animatedFramesDiscarded); + }); + eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL); +} + +void RasterImage::OnSurfaceDiscardedInternal(bool aAnimatedFramesDiscarded) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aAnimatedFramesDiscarded && mAnimationState) { + MOZ_ASSERT(StaticPrefs::image_mem_animated_discardable_AtStartup()); + + IntRect rect = mAnimationState->UpdateState(this, mSize.ToUnknownSize()); + + auto dirtyRect = OrientedIntRect::FromUnknownRect(rect); + NotifyProgress(NoProgress, dirtyRect); + } + + if (mProgressTracker) { + mProgressTracker->OnDiscard(); + } +} + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::GetAnimated(bool* aAnimated) { + if (mError) { + return NS_ERROR_FAILURE; + } + + NS_ENSURE_ARG_POINTER(aAnimated); + + // If we have an AnimationState, we can know for sure. + if (mAnimationState) { + *aAnimated = true; + return NS_OK; + } + + // Otherwise, we need to have been decoded to know for sure, since if we were + // decoded at least once mAnimationState would have been created for animated + // images. This is true even though we check for animation during the + // metadata decode, because we may still discover animation only during the + // full decode for corrupt images. + if (!LoadHasBeenDecoded()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // We know for sure + *aAnimated = false; + + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP_(int32_t) +RasterImage::GetFirstFrameDelay() { + if (mError) { + return -1; + } + + bool animated = false; + if (NS_FAILED(GetAnimated(&animated)) || !animated) { + return -1; + } + + MOZ_ASSERT(mAnimationState, "Animated images should have an AnimationState"); + return mAnimationState->FirstFrameTimeout().AsEncodedValueDeprecated(); +} + +NS_IMETHODIMP_(already_AddRefed) +RasterImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + return GetFrameAtSize(mSize.ToUnknownSize(), aWhichFrame, aFlags); +} + +NS_IMETHODIMP_(already_AddRefed) +RasterImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE); + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + if (aSize.IsEmpty() || aWhichFrame > FRAME_MAX_VALUE || mError) { + return nullptr; + } + + auto size = OrientedIntSize::FromUnknownSize(aSize); + + // Get the frame. If it's not there, it's probably the caller's fault for + // not waiting for the data to be loaded from the network or not passing + // FLAG_SYNC_DECODE. + LookupResult result = LookupFrame(size, aFlags, ToPlaybackType(aWhichFrame), + /* aMarkUsed = */ true); + if (!result) { + // The OS threw this frame away and we couldn't redecode it. + return nullptr; + } + + return result.Surface()->GetSourceSurface(); +} + +NS_IMETHODIMP_(bool) +RasterImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + return LoadHasSize(); +} + +NS_IMETHODIMP_(ImgDrawResult) +RasterImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRenderer); + + if (mError) { + return ImgDrawResult::BAD_IMAGE; + } + + if (!LoadHasSize()) { + return ImgDrawResult::NOT_READY; + } + + if (aSize.IsEmpty()) { + return ImgDrawResult::BAD_ARGS; + } + + // We check the minimum size because while we support downscaling, we do not + // support upscaling. If aRequestedSize > mSize, we will never give a larger + // surface than mSize. If mSize > aRequestedSize, and mSize > maxTextureSize, + // we still want to use image containers if aRequestedSize <= maxTextureSize. + int32_t maxTextureSize = aRenderer->GetMaxTextureSize(); + if (min(mSize.width, aSize.width) > maxTextureSize || + min(mSize.height, aSize.height) > maxTextureSize) { + return ImgDrawResult::NOT_SUPPORTED; + } + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + // Get the frame. If it's not there, it's probably the caller's fault for + // not waiting for the data to be loaded from the network or not passing + // FLAG_SYNC_DECODE. + LookupResult result = LookupFrame(OrientedIntSize::FromUnknownSize(aSize), + aFlags, PlaybackType::eAnimated, + /* aMarkUsed = */ true); + if (!result) { + // The OS threw this frame away and we couldn't redecode it. + return ImgDrawResult::NOT_READY; + } + + if (!result.Surface()->IsFinished()) { + result.Surface().TakeProvider(aProvider); + return ImgDrawResult::INCOMPLETE; + } + + result.Surface().TakeProvider(aProvider); + switch (result.Type()) { + case MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND: + case MatchType::SUBSTITUTE_BECAUSE_PENDING: + return ImgDrawResult::WRONG_SIZE; + default: + return ImgDrawResult::SUCCESS; + } +} + +size_t RasterImage::SizeOfSourceWithComputedFallback( + SizeOfState& aState) const { + return mSourceBuffer->SizeOfIncludingThisWithComputedFallback( + aState.mMallocSizeOf); +} + +bool RasterImage::SetMetadata(const ImageMetadata& aMetadata, + bool aFromMetadataDecode) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + return true; + } + + mResolution = aMetadata.GetResolution(); + + if (aMetadata.HasSize()) { + auto metadataSize = aMetadata.GetSize(); + if (metadataSize.width < 0 || metadataSize.height < 0) { + NS_WARNING("Image has negative intrinsic size"); + DoError(); + return true; + } + + MOZ_ASSERT(aMetadata.HasOrientation()); + Orientation orientation = aMetadata.GetOrientation(); + + // If we already have a size, check the new size against the old one. + if (LoadHasSize() && + (metadataSize != mSize || orientation != mOrientation)) { + NS_WARNING( + "Image changed size or orientation on redecode! " + "This should not happen!"); + DoError(); + return true; + } + + // Set the size and flag that we have it. + mOrientation = orientation; + mSize = metadataSize; + mNativeSizes.Clear(); + for (const auto& nativeSize : aMetadata.GetNativeSizes()) { + mNativeSizes.AppendElement(nativeSize); + } + StoreHasSize(true); + } + + if (LoadHasSize() && aMetadata.HasAnimation() && !mAnimationState) { + // We're becoming animated, so initialize animation stuff. + mAnimationState.emplace(mAnimationMode); + mFrameAnimator = MakeUnique(this, mSize.ToUnknownSize()); + + if (!StaticPrefs::image_mem_animated_discardable_AtStartup()) { + // We don't support discarding animated images (See bug 414259). + // Lock the image and throw away the key. + LockImage(); + } + + if (!aFromMetadataDecode) { + // The metadata decode reported that this image isn't animated, but we + // discovered that it actually was during the full decode. This is a + // rare failure that only occurs for corrupt images. To recover, we need + // to discard all existing surfaces and redecode. + return false; + } + } + + if (mAnimationState) { + mAnimationState->SetLoopCount(aMetadata.GetLoopCount()); + mAnimationState->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout()); + + if (aMetadata.HasLoopLength()) { + mAnimationState->SetLoopLength(aMetadata.GetLoopLength()); + } + if (aMetadata.HasFirstFrameRefreshArea()) { + mAnimationState->SetFirstFrameRefreshArea( + aMetadata.GetFirstFrameRefreshArea()); + } + } + + if (aMetadata.HasHotspot()) { + // NOTE(heycam): We shouldn't have any image formats that support both + // orientation and hotspots, so we assert that rather than add code + // to orient the hotspot point correctly. + MOZ_ASSERT(mOrientation.IsIdentity(), "Would need to orient hotspot point"); + + auto hotspot = aMetadata.GetHotspot(); + mHotspot.x = std::max(std::min(hotspot.x.value, mSize.width - 1), 0); + mHotspot.y = std::max(std::min(hotspot.y.value, mSize.height - 1), 0); + } + + return true; +} + +NS_IMETHODIMP +RasterImage::SetAnimationMode(uint16_t aAnimationMode) { + if (mAnimationState) { + mAnimationState->SetAnimationMode(aAnimationMode); + } + return SetAnimationModeInternal(aAnimationMode); +} + +//****************************************************************************** + +nsresult RasterImage::StartAnimation() { + if (mError) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(ShouldAnimate(), "Should not animate!"); + + // If we're not ready to animate, then set mPendingAnimation, which will cause + // us to start animating if and when we do become ready. + StorePendingAnimation(!mAnimationState || + mAnimationState->KnownFrameCount() < 1); + if (LoadPendingAnimation()) { + return NS_OK; + } + + // Don't bother to animate if we're displaying the first frame forever. + if (mAnimationState->GetCurrentAnimationFrameIndex() == 0 && + mAnimationState->FirstFrameTimeout() == FrameTimeout::Forever()) { + StoreAnimationFinished(true); + return NS_ERROR_ABORT; + } + + // We need to set the time that this initial frame was first displayed, as + // this is used in AdvanceFrame(). + mAnimationState->InitAnimationFrameTimeIfNecessary(); + + return NS_OK; +} + +//****************************************************************************** +nsresult RasterImage::StopAnimation() { + MOZ_ASSERT(mAnimating, "Should be animating!"); + + nsresult rv = NS_OK; + if (mError) { + rv = NS_ERROR_FAILURE; + } else { + mAnimationState->SetAnimationFrameTime(TimeStamp()); + } + + mAnimating = false; + return rv; +} + +//****************************************************************************** +NS_IMETHODIMP +RasterImage::ResetAnimation() { + if (mError) { + return NS_ERROR_FAILURE; + } + + StorePendingAnimation(false); + + if (mAnimationMode == kDontAnimMode || !mAnimationState || + mAnimationState->GetCurrentAnimationFrameIndex() == 0) { + return NS_OK; + } + + StoreAnimationFinished(false); + + if (mAnimating) { + StopAnimation(); + } + + MOZ_ASSERT(mAnimationState, "Should have AnimationState"); + MOZ_ASSERT(mFrameAnimator, "Should have FrameAnimator"); + mFrameAnimator->ResetAnimation(*mAnimationState); + + IntRect area = mAnimationState->FirstFrameRefreshArea(); + NotifyProgress(NoProgress, OrientedIntRect::FromUnknownRect(area)); + + // Start the animation again. It may not have been running before, if + // mAnimationFinished was true before entering this function. + EvaluateAnimation(); + + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP_(void) +RasterImage::SetAnimationStartTime(const TimeStamp& aTime) { + if (mError || mAnimationMode == kDontAnimMode || mAnimating || + !mAnimationState) { + return; + } + + mAnimationState->SetAnimationFrameTime(aTime); +} + +NS_IMETHODIMP_(float) +RasterImage::GetFrameIndex(uint32_t aWhichFrame) { + MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument"); + return (aWhichFrame == FRAME_FIRST || !mAnimationState) + ? 0.0f + : mAnimationState->GetCurrentAnimationFrameIndex(); +} + +NS_IMETHODIMP_(IntRect) +RasterImage::GetImageSpaceInvalidationRect(const IntRect& aRect) { + // Note that we do not transform aRect into an UnorientedIntRect, since + // RasterImage::NotifyProgress notifies all consumers of the image using + // OrientedIntRect values. (This is unlike OrientedImage, which notifies + // using inner image coordinates.) + return aRect; +} + +nsresult RasterImage::OnImageDataComplete(nsIRequest*, nsresult aStatus, + bool aLastPart) { + MOZ_ASSERT(NS_IsMainThread()); + + // Record that we have all the data we're going to get now. + StoreAllSourceData(true); + + // Let decoders know that there won't be any more data coming. + mSourceBuffer->Complete(aStatus); + + // Allow a synchronous metadata decode if mSyncLoad was set, or if we're + // running on a single thread (in which case waiting for the async metadata + // decoder could delay this image's load event quite a bit), or if this image + // is transient. + bool canSyncDecodeMetadata = + LoadSyncLoad() || LoadTransient() || DecodePool::NumberOfCores() < 2; + + if (canSyncDecodeMetadata && !LoadHasSize()) { + // We're loading this image synchronously, so it needs to be usable after + // this call returns. Since we haven't gotten our size yet, we need to do a + // synchronous metadata decode here. + DecodeMetadata(FLAG_SYNC_DECODE); + } + + // Determine our final status, giving precedence to Necko failure codes. We + // check after running the metadata decode in case it triggered an error. + nsresult finalStatus = mError ? NS_ERROR_FAILURE : NS_OK; + if (NS_FAILED(aStatus)) { + finalStatus = aStatus; + } + + // If loading failed, report an error. + if (NS_FAILED(finalStatus)) { + DoError(); + } + + Progress loadProgress = LoadCompleteProgress(aLastPart, mError, finalStatus); + + if (!LoadHasSize() && !mError) { + // We don't have our size yet, so we'll fire the load event in SetSize(). + MOZ_ASSERT(!canSyncDecodeMetadata, + "Firing load async after metadata sync decode?"); + mLoadProgress = Some(loadProgress); + return finalStatus; + } + + NotifyForLoadEvent(loadProgress); + + return finalStatus; +} + +void RasterImage::NotifyForLoadEvent(Progress aProgress) { + MOZ_ASSERT(LoadHasSize() || mError, + "Need to know size before firing load event"); + MOZ_ASSERT( + !LoadHasSize() || (mProgressTracker->GetProgress() & FLAG_SIZE_AVAILABLE), + "Should have notified that the size is available if we have it"); + + // If we encountered an error, make sure we notify for that as well. + if (mError) { + aProgress |= FLAG_HAS_ERROR; + } + + // Notify our listeners, which will fire this image's load event. + NotifyProgress(aProgress); +} + +nsresult RasterImage::OnImageDataAvailable(nsIRequest*, + nsIInputStream* aInputStream, + uint64_t, uint32_t aCount) { + nsresult rv = mSourceBuffer->AppendFromInputStream(aInputStream, aCount); + if (NS_SUCCEEDED(rv) && !LoadSomeSourceData()) { + StoreSomeSourceData(true); + if (!LoadSyncLoad()) { + // Create an async metadata decoder and verify we succeed in doing so. + rv = DecodeMetadata(DECODE_FLAGS_DEFAULT); + } + } + + if (NS_FAILED(rv)) { + DoError(); + } + return rv; +} + +nsresult RasterImage::SetSourceSizeHint(uint32_t aSizeHint) { + if (aSizeHint == 0) { + return NS_OK; + } + + nsresult rv = mSourceBuffer->ExpectLength(aSizeHint); + if (rv == NS_ERROR_OUT_OF_MEMORY) { + // Flush memory, try to get some back, and try again. + rv = nsMemory::HeapMinimize(true); + if (NS_SUCCEEDED(rv)) { + rv = mSourceBuffer->ExpectLength(aSizeHint); + } + } + + return rv; +} + +nsresult RasterImage::GetHotspotX(int32_t* aX) { + *aX = mHotspot.x; + return NS_OK; +} + +nsresult RasterImage::GetHotspotY(int32_t* aY) { + *aY = mHotspot.y; + return NS_OK; +} + +void RasterImage::Discard() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(CanDiscard(), "Asked to discard but can't"); + MOZ_ASSERT(!mAnimationState || + StaticPrefs::image_mem_animated_discardable_AtStartup(), + "Asked to discard for animated image"); + + // Delete all the decoded frames. + SurfaceCache::RemoveImage(ImageKey(this)); + + if (mAnimationState) { + IntRect rect = mAnimationState->UpdateState(this, mSize.ToUnknownSize()); + + auto dirtyRect = OrientedIntRect::FromUnknownRect(rect); + NotifyProgress(NoProgress, dirtyRect); + } + + // Notify that we discarded. + if (mProgressTracker) { + mProgressTracker->OnDiscard(); + } +} + +bool RasterImage::CanDiscard() { + return LoadAllSourceData() && + // Can discard animated images if the pref is set + (!mAnimationState || + StaticPrefs::image_mem_animated_discardable_AtStartup()); +} + +NS_IMETHODIMP +RasterImage::StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) { + if (mError) { + return NS_ERROR_FAILURE; + } + + if (!LoadHasSize()) { + StoreWantFullDecode(true); + return NS_OK; + } + + uint32_t flags = (aFlags & FLAG_ASYNC_NOTIFY) | FLAG_SYNC_DECODE_IF_FAST | + FLAG_HIGH_QUALITY_SCALING; + return RequestDecodeForSize(mSize.ToUnknownSize(), flags, aWhichFrame); +} + +bool RasterImage::StartDecodingWithResult(uint32_t aFlags, + uint32_t aWhichFrame) { + if (mError) { + return false; + } + + if (!LoadHasSize()) { + StoreWantFullDecode(true); + return false; + } + + uint32_t flags = (aFlags & FLAG_ASYNC_NOTIFY) | FLAG_SYNC_DECODE_IF_FAST | + FLAG_HIGH_QUALITY_SCALING; + LookupResult result = RequestDecodeForSizeInternal(mSize, flags, aWhichFrame); + DrawableSurface surface = std::move(result.Surface()); + return surface && surface->IsFinished(); +} + +bool RasterImage::HasDecodedPixels() { + LookupResult result = SurfaceCache::LookupBestMatch( + ImageKey(this), + RasterSurfaceKey(mSize.ToUnknownSize(), DefaultSurfaceFlags(), + PlaybackType::eStatic), + /* aMarkUsed = */ false); + MatchType matchType = result.Type(); + if (matchType == MatchType::NOT_FOUND || matchType == MatchType::PENDING || + !bool(result.Surface())) { + return false; + } + + return !result.Surface()->GetDecodedRect().IsEmpty(); +} + +imgIContainer::DecodeResult RasterImage::RequestDecodeWithResult( + uint32_t aFlags, uint32_t aWhichFrame) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + return imgIContainer::DECODE_REQUEST_FAILED; + } + + uint32_t flags = aFlags | FLAG_ASYNC_NOTIFY; + LookupResult result = RequestDecodeForSizeInternal(mSize, flags, aWhichFrame); + DrawableSurface surface = std::move(result.Surface()); + if (surface && surface->IsFinished()) { + return imgIContainer::DECODE_SURFACE_AVAILABLE; + } + if (result.GetFailedToRequestDecode()) { + return imgIContainer::DECODE_REQUEST_FAILED; + } + return imgIContainer::DECODE_REQUESTED; +} + +NS_IMETHODIMP +RasterImage::RequestDecodeForSize(const IntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + return NS_ERROR_FAILURE; + } + + RequestDecodeForSizeInternal(OrientedIntSize::FromUnknownSize(aSize), aFlags, + aWhichFrame); + + return NS_OK; +} + +LookupResult RasterImage::RequestDecodeForSizeInternal( + const OrientedIntSize& aSize, uint32_t aFlags, uint32_t aWhichFrame) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aWhichFrame > FRAME_MAX_VALUE) { + return LookupResult(MatchType::NOT_FOUND); + } + + if (mError) { + LookupResult result = LookupResult(MatchType::NOT_FOUND); + result.SetFailedToRequestDecode(); + return result; + } + + if (!LoadHasSize()) { + StoreWantFullDecode(true); + return LookupResult(MatchType::NOT_FOUND); + } + + // Decide whether to sync decode images we can decode quickly. Here we are + // explicitly trading off flashing for responsiveness in the case that we're + // redecoding an image (see bug 845147). + bool shouldSyncDecodeIfFast = + !LoadHasBeenDecoded() && (aFlags & FLAG_SYNC_DECODE_IF_FAST); + + uint32_t flags = + shouldSyncDecodeIfFast ? aFlags : aFlags & ~FLAG_SYNC_DECODE_IF_FAST; + + // Perform a frame lookup, which will implicitly start decoding if needed. + return LookupFrame(aSize, flags, ToPlaybackType(aWhichFrame), + /* aMarkUsed = */ false); +} + +static bool LaunchDecodingTask(IDecodingTask* aTask, RasterImage* aImage, + uint32_t aFlags, bool aHaveSourceData) { + if (aHaveSourceData) { + nsCString uri(aImage->GetURIString()); + + // If we have all the data, we can sync decode if requested. + if (aFlags & imgIContainer::FLAG_SYNC_DECODE) { + DecodePool::Singleton()->SyncRunIfPossible(aTask, uri); + return true; + } + + if (aFlags & imgIContainer::FLAG_SYNC_DECODE_IF_FAST) { + return DecodePool::Singleton()->SyncRunIfPreferred(aTask, uri); + } + } + + // Perform an async decode. We also take this path if we don't have all the + // source data yet, since sync decoding is impossible in that situation. + DecodePool::Singleton()->AsyncRun(aTask); + return false; +} + +void RasterImage::Decode(const OrientedIntSize& aSize, uint32_t aFlags, + PlaybackType aPlaybackType, bool& aOutRanSync, + bool& aOutFailed) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + aOutFailed = true; + return; + } + + // If we don't have a size yet, we can't do any other decoding. + if (!LoadHasSize()) { + StoreWantFullDecode(true); + return; + } + + // We're about to decode again, which may mean that some of the previous sizes + // we've decoded at aren't useful anymore. We can allow them to expire from + // the cache by unlocking them here. When the decode finishes, it will send an + // invalidation that will cause all instances of this image to redraw. If this + // image is locked, any surfaces that are still useful will become locked + // again when LookupFrame touches them, and the remainder will eventually + // expire. + SurfaceCache::UnlockEntries(ImageKey(this)); + + // Determine which flags we need to decode this image with. + DecoderFlags decoderFlags = mDefaultDecoderFlags; + if (aFlags & FLAG_ASYNC_NOTIFY) { + decoderFlags |= DecoderFlags::ASYNC_NOTIFY; + } + if (LoadTransient()) { + decoderFlags |= DecoderFlags::IMAGE_IS_TRANSIENT; + } + if (LoadHasBeenDecoded()) { + decoderFlags |= DecoderFlags::IS_REDECODE; + } + if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) { + // Used SurfaceCache::Lookup instead of SurfaceCache::LookupBestMatch. That + // means the caller can handle a differently sized surface to be returned + // at any point. + decoderFlags |= DecoderFlags::CANNOT_SUBSTITUTE; + } + + SurfaceFlags surfaceFlags = ToSurfaceFlags(aFlags); + if (IsOpaque()) { + // If there's no transparency, it doesn't matter whether we premultiply + // alpha or not. + surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA; + } + + // Create a decoder. + RefPtr task; + nsresult rv; + bool animated = mAnimationState && aPlaybackType == PlaybackType::eAnimated; + if (animated) { + size_t currentFrame = mAnimationState->GetCurrentAnimationFrameIndex(); + rv = DecoderFactory::CreateAnimationDecoder( + mDecoderType, WrapNotNull(this), mSourceBuffer, mSize.ToUnknownSize(), + decoderFlags, surfaceFlags, currentFrame, getter_AddRefs(task)); + } else { + rv = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this), + mSourceBuffer, mSize.ToUnknownSize(), + aSize.ToUnknownSize(), decoderFlags, + surfaceFlags, getter_AddRefs(task)); + } + + if (rv == NS_ERROR_ALREADY_INITIALIZED) { + // We raced with an already pending decoder, and it finished before we + // managed to insert the new decoder. Pretend we did a sync call to make + // the caller lookup in the surface cache again. + MOZ_ASSERT(!task); + aOutRanSync = true; + return; + } + + if (animated) { + // We pass false for aAllowInvalidation because we may be asked to use + // async notifications. Any potential invalidation here will be sent when + // RequestRefresh is called, or NotifyDecodeComplete. +#ifdef DEBUG + IntRect rect = +#endif + mAnimationState->UpdateState(this, mSize.ToUnknownSize(), false); + MOZ_ASSERT(rect.IsEmpty()); + } + + // Make sure DecoderFactory was able to create a decoder successfully. + if (NS_FAILED(rv)) { + MOZ_ASSERT(!task); + aOutFailed = true; + return; + } + + MOZ_ASSERT(task); + mDecodeCount++; + + // We're ready to decode; start the decoder. + aOutRanSync = LaunchDecodingTask(task, this, aFlags, LoadAllSourceData()); +} + +NS_IMETHODIMP +RasterImage::DecodeMetadata(uint32_t aFlags) { + if (mError) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(!LoadHasSize(), "Should not do unnecessary metadata decodes"); + + // Create a decoder. + RefPtr task = DecoderFactory::CreateMetadataDecoder( + mDecoderType, WrapNotNull(this), mDefaultDecoderFlags, mSourceBuffer); + + // Make sure DecoderFactory was able to create a decoder successfully. + if (!task) { + return NS_ERROR_FAILURE; + } + + // We're ready to decode; start the decoder. + LaunchDecodingTask(task, this, aFlags, LoadAllSourceData()); + return NS_OK; +} + +void RasterImage::RecoverFromInvalidFrames(const OrientedIntSize& aSize, + uint32_t aFlags) { + if (!LoadHasSize()) { + return; + } + + NS_WARNING("A RasterImage's frames became invalid. Attempting to recover..."); + + // Discard all existing frames, since they're probably all now invalid. + SurfaceCache::RemoveImage(ImageKey(this)); + + // Relock the image if it's supposed to be locked. + if (mLockCount > 0) { + SurfaceCache::LockImage(ImageKey(this)); + } + + bool unused1, unused2; + + // Animated images require some special handling, because we normally require + // that they never be discarded. + if (mAnimationState) { + Decode(mSize, aFlags | FLAG_SYNC_DECODE, PlaybackType::eAnimated, unused1, + unused2); + ResetAnimation(); + return; + } + + // For non-animated images, it's fine to recover using an async decode. + Decode(aSize, aFlags, PlaybackType::eStatic, unused1, unused2); +} + +bool RasterImage::CanDownscaleDuringDecode(const OrientedIntSize& aSize, + uint32_t aFlags) { + // Check basic requirements: downscale-during-decode is enabled, Skia is + // available, this image isn't transient, we have all the source data and know + // our size, and the flags allow us to do it. + if (!LoadHasSize() || LoadTransient() || + !StaticPrefs::image_downscale_during_decode_enabled() || + !(aFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING)) { + return false; + } + + // We don't downscale animated images during decode. + if (mAnimationState) { + return false; + } + + // Never upscale. + if (aSize.width >= mSize.width || aSize.height >= mSize.height) { + return false; + } + + // Zero or negative width or height is unacceptable. + if (aSize.width < 1 || aSize.height < 1) { + return false; + } + + // There's no point in scaling if we can't store the result. + if (!SurfaceCache::CanHold(aSize.ToUnknownSize())) { + return false; + } + + return true; +} + +ImgDrawResult RasterImage::DrawInternal(DrawableSurface&& aSurface, + gfxContext* aContext, + const OrientedIntSize& aSize, + const ImageRegion& aRegion, + SamplingFilter aSamplingFilter, + uint32_t aFlags, float aOpacity) { + gfxContextMatrixAutoSaveRestore saveMatrix(aContext); + ImageRegion region(aRegion); + bool frameIsFinished = aSurface->IsFinished(); + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + // By now we may have a frame with the requested size. If not, we need to + // adjust the drawing parameters accordingly. + IntSize finalSize = aSurface->GetSize(); + bool couldRedecodeForBetterFrame = false; + if (finalSize != aSize.ToUnknownSize()) { + gfx::MatrixScales scale(double(aSize.width) / finalSize.width, + double(aSize.height) / finalSize.height); + aContext->Multiply(gfx::Matrix::Scaling(scale)); + region.Scale(1.0 / scale.xScale, 1.0 / scale.yScale); + + couldRedecodeForBetterFrame = CanDownscaleDuringDecode(aSize, aFlags); + } + + if (!aSurface->Draw(aContext, region, aSamplingFilter, aFlags, aOpacity)) { + RecoverFromInvalidFrames(aSize, aFlags); + return ImgDrawResult::TEMPORARY_ERROR; + } + if (!frameIsFinished) { + return ImgDrawResult::INCOMPLETE; + } + if (couldRedecodeForBetterFrame) { + return ImgDrawResult::WRONG_SIZE; + } + return ImgDrawResult::SUCCESS; +} + +//****************************************************************************** +NS_IMETHODIMP_(ImgDrawResult) +RasterImage::Draw(gfxContext* aContext, const IntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& /*aSVGContext - ignored*/, + uint32_t aFlags, float aOpacity) { + if (aWhichFrame > FRAME_MAX_VALUE) { + return ImgDrawResult::BAD_ARGS; + } + + if (mError) { + return ImgDrawResult::BAD_IMAGE; + } + + // Illegal -- you can't draw with non-default decode flags. + // (Disabling colorspace conversion might make sense to allow, but + // we don't currently.) + if (ToSurfaceFlags(aFlags) != DefaultSurfaceFlags()) { + return ImgDrawResult::BAD_ARGS; + } + + if (!aContext) { + return ImgDrawResult::BAD_ARGS; + } + + if (mAnimationConsumers == 0 && mAnimationState) { + SendOnUnlockedDraw(aFlags); + } + + // If we're not using SamplingFilter::GOOD, we shouldn't high-quality scale or + // downscale during decode. + uint32_t flags = aSamplingFilter == SamplingFilter::GOOD + ? aFlags + : aFlags & ~FLAG_HIGH_QUALITY_SCALING; + + auto size = OrientedIntSize::FromUnknownSize(aSize); + LookupResult result = LookupFrame(size, flags, ToPlaybackType(aWhichFrame), + /* aMarkUsed = */ true); + if (!result) { + // Getting the frame (above) touches the image and kicks off decoding. + if (mDrawStartTime.IsNull()) { + mDrawStartTime = TimeStamp::Now(); + } + return ImgDrawResult::NOT_READY; + } + + bool shouldRecordTelemetry = + !mDrawStartTime.IsNull() && result.Surface()->IsFinished(); + + ImgDrawResult drawResult = + DrawInternal(std::move(result.Surface()), aContext, size, aRegion, + aSamplingFilter, flags, aOpacity); + + if (shouldRecordTelemetry) { + TimeDuration drawLatency = TimeStamp::Now() - mDrawStartTime; + Telemetry::Accumulate(Telemetry::IMAGE_DECODE_ON_DRAW_LATENCY, + int32_t(drawLatency.ToMicroseconds())); + mDrawStartTime = TimeStamp(); + } + + return drawResult; +} + +//****************************************************************************** + +NS_IMETHODIMP +RasterImage::LockImage() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread to encourage serialization with UnlockImage"); + if (mError) { + return NS_ERROR_FAILURE; + } + + // Increment the lock count + mLockCount++; + + // Lock this image's surfaces in the SurfaceCache. + if (mLockCount == 1) { + SurfaceCache::LockImage(ImageKey(this)); + } + + return NS_OK; +} + +//****************************************************************************** + +NS_IMETHODIMP +RasterImage::UnlockImage() { + MOZ_ASSERT(NS_IsMainThread(), + "Main thread to encourage serialization with LockImage"); + if (mError) { + return NS_ERROR_FAILURE; + } + + // It's an error to call this function if the lock count is 0 + MOZ_ASSERT(mLockCount > 0, "Calling UnlockImage with mLockCount == 0!"); + if (mLockCount == 0) { + return NS_ERROR_ABORT; + } + + // Decrement our lock count + mLockCount--; + + // Unlock this image's surfaces in the SurfaceCache. + if (mLockCount == 0) { + SurfaceCache::UnlockImage(ImageKey(this)); + } + + return NS_OK; +} + +//****************************************************************************** + +NS_IMETHODIMP +RasterImage::RequestDiscard() { + if (LoadDiscardable() && // Enabled at creation time... + mLockCount == 0 && // ...not temporarily disabled... + CanDiscard()) { + Discard(); + } + + return NS_OK; +} + +// Idempotent error flagging routine. If a decoder is open, shuts it down. +void RasterImage::DoError() { + // If we've flagged an error before, we have nothing to do + if (mError) { + return; + } + + // We can't safely handle errors off-main-thread, so dispatch a worker to + // do it. + if (!NS_IsMainThread()) { + HandleErrorWorker::DispatchIfNeeded(this); + return; + } + + // Put the container in an error state. + mError = true; + + // Stop animation and release our FrameAnimator. + if (mAnimating) { + StopAnimation(); + } + mAnimationState = Nothing(); + mFrameAnimator = nullptr; + + // Release all locks. + mLockCount = 0; + SurfaceCache::UnlockImage(ImageKey(this)); + + // Release all frames from the surface cache. + SurfaceCache::RemoveImage(ImageKey(this)); + + // Invalidate to get rid of any partially-drawn image content. + auto dirtyRect = OrientedIntRect({0, 0}, mSize); + NotifyProgress(NoProgress, dirtyRect); + + MOZ_LOG(gImgLog, LogLevel::Error, + ("RasterImage: [this=%p] Error detected for image\n", this)); +} + +/* static */ +void RasterImage::HandleErrorWorker::DispatchIfNeeded(RasterImage* aImage) { + RefPtr worker = new HandleErrorWorker(aImage); + NS_DispatchToMainThread(worker); +} + +RasterImage::HandleErrorWorker::HandleErrorWorker(RasterImage* aImage) + : Runnable("image::RasterImage::HandleErrorWorker"), mImage(aImage) { + MOZ_ASSERT(mImage, "Should have image"); +} + +NS_IMETHODIMP +RasterImage::HandleErrorWorker::Run() { + mImage->DoError(); + + return NS_OK; +} + +bool RasterImage::ShouldAnimate() { + return ImageResource::ShouldAnimate() && mAnimationState && + mAnimationState->KnownFrameCount() >= 1 && !LoadAnimationFinished(); +} + +#ifdef DEBUG +NS_IMETHODIMP +RasterImage::GetFramesNotified(uint32_t* aFramesNotified) { + NS_ENSURE_ARG_POINTER(aFramesNotified); + + *aFramesNotified = mFramesNotified; + + return NS_OK; +} +#endif + +void RasterImage::NotifyProgress( + Progress aProgress, + const OrientedIntRect& aInvalidRect /* = OrientedIntRect() */, + const Maybe& aFrameCount /* = Nothing() */, + DecoderFlags aDecoderFlags /* = DefaultDecoderFlags() */, + SurfaceFlags aSurfaceFlags /* = DefaultSurfaceFlags() */) { + MOZ_ASSERT(NS_IsMainThread()); + + // Ensure that we stay alive long enough to finish notifying. + RefPtr image = this; + + OrientedIntRect invalidRect = aInvalidRect; + + if (!(aDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY)) { + // We may have decoded new animation frames; update our animation state. + MOZ_ASSERT_IF(aFrameCount && *aFrameCount > 1, mAnimationState || mError); + if (mAnimationState && aFrameCount) { + mAnimationState->UpdateKnownFrameCount(*aFrameCount); + } + + // If we should start animating right now, do so. + if (mAnimationState && aFrameCount == Some(1u) && LoadPendingAnimation() && + ShouldAnimate()) { + StartAnimation(); + } + + if (mAnimationState) { + IntRect rect = mAnimationState->UpdateState(this, mSize.ToUnknownSize()); + + invalidRect.UnionRect(invalidRect, + OrientedIntRect::FromUnknownRect(rect)); + } + } + + // Tell the observers what happened. + image->mProgressTracker->SyncNotifyProgress(aProgress, + invalidRect.ToUnknownRect()); +} + +void RasterImage::NotifyDecodeComplete( + const DecoderFinalStatus& aStatus, const ImageMetadata& aMetadata, + const DecoderTelemetry& aTelemetry, Progress aProgress, + const OrientedIntRect& aInvalidRect, const Maybe& aFrameCount, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags) { + MOZ_ASSERT(NS_IsMainThread()); + + // If the decoder detected an error, log it to the error console. + if (aStatus.mShouldReportError) { + ReportDecoderError(); + } + + // Record all the metadata the decoder gathered about this image. + bool metadataOK = SetMetadata(aMetadata, aStatus.mWasMetadataDecode); + if (!metadataOK) { + // This indicates a serious error that requires us to discard all existing + // surfaces and redecode to recover. We'll drop the results from this + // decoder on the floor, since they aren't valid. + RecoverFromInvalidFrames(mSize, FromSurfaceFlags(aSurfaceFlags)); + return; + } + + MOZ_ASSERT(mError || LoadHasSize() || !aMetadata.HasSize(), + "SetMetadata should've gotten a size"); + + if (!aStatus.mWasMetadataDecode && aStatus.mFinished) { + // Flag that we've been decoded before. + StoreHasBeenDecoded(true); + } + + // Send out any final notifications. + NotifyProgress(aProgress, aInvalidRect, aFrameCount, aDecoderFlags, + aSurfaceFlags); + + if (!(aDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY)) { + // We may have decoded new animation frames; update our animation state. + MOZ_ASSERT_IF(aFrameCount && *aFrameCount > 1, mAnimationState || mError); + if (mAnimationState && aFrameCount) { + mAnimationState->UpdateKnownFrameCount(*aFrameCount); + } + + // If we should start animating right now, do so. + if (mAnimationState && aFrameCount == Some(1u) && LoadPendingAnimation() && + ShouldAnimate()) { + StartAnimation(); + } + + if (mAnimationState && LoadHasBeenDecoded()) { + // We've finished a full decode of all animation frames and our + // AnimationState has been notified about them all, so let it know not to + // expect anymore. + mAnimationState->NotifyDecodeComplete(); + + IntRect rect = mAnimationState->UpdateState(this, mSize.ToUnknownSize()); + + if (!rect.IsEmpty()) { + auto dirtyRect = OrientedIntRect::FromUnknownRect(rect); + NotifyProgress(NoProgress, dirtyRect); + } + } + } + + // Do some telemetry if this isn't a metadata decode. + if (!aStatus.mWasMetadataDecode) { + if (aTelemetry.mChunkCount) { + Telemetry::Accumulate(Telemetry::IMAGE_DECODE_CHUNKS, + aTelemetry.mChunkCount); + } + + if (aStatus.mFinished) { + Telemetry::Accumulate(Telemetry::IMAGE_DECODE_TIME, + int32_t(aTelemetry.mDecodeTime.ToMicroseconds())); + + if (aTelemetry.mSpeedHistogram && aTelemetry.mBytesDecoded) { + Telemetry::Accumulate(*aTelemetry.mSpeedHistogram, aTelemetry.Speed()); + } + } + } + + // Only act on errors if we have no usable frames from the decoder. + if (aStatus.mHadError && + (!mAnimationState || mAnimationState->KnownFrameCount() == 0)) { + DoError(); + } else if (aStatus.mWasMetadataDecode && !LoadHasSize()) { + DoError(); + } + + // XXX(aosmond): Can we get this far without mFinished == true? + if (aStatus.mFinished && aStatus.mWasMetadataDecode) { + // If we were waiting to fire the load event, go ahead and fire it now. + if (mLoadProgress) { + NotifyForLoadEvent(*mLoadProgress); + mLoadProgress = Nothing(); + } + + // If we were a metadata decode and a full decode was requested, do it. + if (LoadWantFullDecode()) { + StoreWantFullDecode(false); + RequestDecodeForSizeInternal(mSize, + DECODE_FLAGS_DEFAULT | + FLAG_HIGH_QUALITY_SCALING | + FLAG_AVOID_REDECODE_FOR_SIZE, + FRAME_CURRENT); + } + } +} + +void RasterImage::ReportDecoderError() { + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + nsCOMPtr errorObject = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + + if (consoleService && errorObject) { + nsAutoString msg(u"Image corrupt or truncated."_ns); + nsAutoString src; + if (GetURI()) { + nsAutoCString uri; + if (!GetSpecTruncatedTo1k(uri)) { + msg += u" URI in this note truncated due to length."_ns; + } + CopyUTF8toUTF16(uri, src); + } + if (NS_SUCCEEDED(errorObject->InitWithWindowID(msg, src, u""_ns, 0, 0, + nsIScriptError::errorFlag, + "Image", InnerWindowID()))) { + consoleService->LogMessage(errorObject); + } + } +} + +already_AddRefed RasterImage::Unwrap() { + nsCOMPtr self(this); + return self.forget(); +} + +void RasterImage::PropagateUseCounters(dom::Document*) { + // No use counters. +} + +IntSize RasterImage::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + MOZ_ASSERT(aDest.width >= 0 || ceil(aDest.width) <= INT32_MAX || + aDest.height >= 0 || ceil(aDest.height) <= INT32_MAX, + "Unexpected destination size"); + + if (mSize.IsEmpty() || aDest.IsEmpty()) { + return IntSize(0, 0); + } + + auto dest = OrientedIntSize::FromUnknownSize( + IntSize::Ceil(aDest.width, aDest.height)); + + if (aSamplingFilter == SamplingFilter::GOOD && + CanDownscaleDuringDecode(dest, aFlags)) { + return dest.ToUnknownSize(); + } + + // We can't scale to this size. Use our intrinsic size for now. + return mSize.ToUnknownSize(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/RasterImage.h b/image/RasterImage.h new file mode 100644 index 0000000000..28cc56e54e --- /dev/null +++ b/image/RasterImage.h @@ -0,0 +1,484 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** @file + * This file declares the RasterImage class, which + * handles static and animated rasterized images. + * + * @author Stuart Parmenter + * @author Chris Saari + * @author Arron Mogge + * @author Andrew Smith + */ + +#ifndef mozilla_image_RasterImage_h +#define mozilla_image_RasterImage_h + +#include "Image.h" +#include "nsCOMPtr.h" +#include "imgIContainer.h" +#include "nsTArray.h" +#include "LookupResult.h" +#include "nsThreadUtils.h" +#include "DecoderFactory.h" +#include "FrameAnimator.h" +#include "ImageMetadata.h" +#include "ISurfaceProvider.h" +#include "Orientation.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/NotNull.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/image/Resolution.h" +#include "ImageContainer.h" +#include "PlaybackType.h" +#ifdef DEBUG +# include "imgIContainerDebug.h" +#endif + +class nsIInputStream; +class nsIRequest; + +#define NS_RASTERIMAGE_CID \ + { /* 376ff2c1-9bf6-418a-b143-3340c00112f7 */ \ + 0x376ff2c1, 0x9bf6, 0x418a, { \ + 0xb1, 0x43, 0x33, 0x40, 0xc0, 0x01, 0x12, 0xf7 \ + } \ + } + +/** + * Handles static and animated image containers. + * + * + * @par A Quick Walk Through + * The decoder initializes this class and calls AppendFrame() to add a frame. + * Once RasterImage detects more than one frame, it starts the animation + * with StartAnimation(). Note that the invalidation events for RasterImage are + * generated automatically using nsRefreshDriver. + * + * @par + * StartAnimation() initializes the animation helper object and sets the time + * the first frame was displayed to the current clock time. + * + * @par + * When the refresh driver corresponding to the imgIContainer that this image is + * a part of notifies the RasterImage that it's time to invalidate, + * RequestRefresh() is called with a given TimeStamp to advance to. As long as + * the timeout of the given frame (the frame's "delay") plus the time that frame + * was first displayed is less than or equal to the TimeStamp given, + * RequestRefresh() calls AdvanceFrame(). + * + * @par + * AdvanceFrame() is responsible for advancing a single frame of the animation. + * It can return true, meaning that the frame advanced, or false, meaning that + * the frame failed to advance (usually because the next frame hasn't been + * decoded yet). It is also responsible for performing the final animation stop + * procedure if the final frame of a non-looping animation is reached. + * + * @par + * Each frame can have a different method of removing itself. These are + * listed as imgIContainer::cDispose... constants. Notify() calls + * DoComposite() to handle any special frame destruction. + * + * @par + * The basic path through DoComposite() is: + * 1) Calculate Area that needs updating, which is at least the area of + * aNextFrame. + * 2) Dispose of previous frame. + * 3) Draw new image onto compositingFrame. + * See comments in DoComposite() for more information and optimizations. + * + * @par + * The rest of the RasterImage specific functions are used by DoComposite to + * destroy the old frame and build the new one. + * + * @note + *
  • "Mask", "Alpha", and "Alpha Level" are interchangeable phrases in + * respects to RasterImage. + * + * @par + *
  • GIFs never have more than a 1 bit alpha. + *
  • APNGs may have a full alpha channel. + * + * @par + *
  • Background color specified in GIF is ignored by web browsers. + * + * @par + *
  • If Frame 3 wants to dispose by restoring previous, what it wants is to + * restore the composition up to and including Frame 2, as well as Frame 2s + * disposal. So, in the middle of DoComposite when composing Frame 3, right + * after destroying Frame 2's area, we copy compositingFrame to + * prevCompositingFrame. When DoComposite gets called to do Frame 4, we + * copy prevCompositingFrame back, and then draw Frame 4 on top. + * + * @par + * The mAnim structure has members only needed for animated images, so + * it's not allocated until the second frame is added. + */ + +namespace mozilla { +namespace layers { +class ImageContainer; +class Image; +class LayersManager; +} // namespace layers + +namespace image { + +class Decoder; +struct DecoderFinalStatus; +struct DecoderTelemetry; +class ImageMetadata; +class SourceBuffer; + +class RasterImage final : public ImageResource, + public SupportsWeakPtr +#ifdef DEBUG + , + public imgIContainerDebug +#endif +{ + // (no public constructor - use ImageFactory) + virtual ~RasterImage(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGICONTAINER +#ifdef DEBUG + NS_DECL_IMGICONTAINERDEBUG +#endif + + virtual nsresult StartAnimation() override; + virtual nsresult StopAnimation() override; + + // Methods inherited from Image + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override; + + virtual size_t SizeOfSourceWithComputedFallback( + SizeOfState& aState) const override; + + /* Triggers discarding. */ + void Discard(); + + ////////////////////////////////////////////////////////////////////////////// + // Decoder callbacks. + ////////////////////////////////////////////////////////////////////////////// + + /** + * Sends the provided progress notifications to ProgressTracker. + * + * Main-thread only. + * + * @param aProgress The progress notifications to send. + * @param aInvalidRect An invalidation rect to send. + * @param aFrameCount If Some(), an updated count of the number of frames of + * animation the decoder has finished decoding so far. + * This is a lower bound for the total number of + * animation frames this image has. + * @param aDecoderFlags The decoder flags used by the decoder that generated + * these notifications, or DefaultDecoderFlags() if the + * notifications don't come from a decoder. + * @param aSurfaceFlags The surface flags used by the decoder that generated + * these notifications, or DefaultSurfaceFlags() if the + * notifications don't come from a decoder. + */ + void NotifyProgress(Progress aProgress, + const OrientedIntRect& aInvalidRect = OrientedIntRect(), + const Maybe& aFrameCount = Nothing(), + DecoderFlags aDecoderFlags = DefaultDecoderFlags(), + SurfaceFlags aSurfaceFlags = DefaultSurfaceFlags()); + + /** + * Records decoding results, sends out any final notifications, updates the + * state of this image, and records telemetry. + * + * Main-thread only. + * + * @param aStatus Final status information about the decoder. (Whether + * it encountered an error, etc.) + * @param aMetadata Metadata about this image that the decoder gathered. + * @param aTelemetry Telemetry data about the decoder. + * @param aProgress Any final progress notifications to send. + * @param aInvalidRect Any final invalidation rect to send. + * @param aFrameCount If Some(), a final updated count of the number of + * frames of animation the decoder has finished decoding so far. This is a + * lower bound for the total number of animation frames this image has. + * @param aDecoderFlags The decoder flags used by the decoder. + * @param aSurfaceFlags The surface flags used by the decoder. + */ + void NotifyDecodeComplete( + const DecoderFinalStatus& aStatus, const ImageMetadata& aMetadata, + const DecoderTelemetry& aTelemetry, Progress aProgress, + const OrientedIntRect& aInvalidRect, const Maybe& aFrameCount, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags); + + // Helper method for NotifyDecodeComplete. + void ReportDecoderError(); + + ////////////////////////////////////////////////////////////////////////////// + // Network callbacks. + ////////////////////////////////////////////////////////////////////////////// + + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) override; + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aStatus, + bool aLastPart) override; + + void NotifyForLoadEvent(Progress aProgress); + + /** + * A hint of the number of bytes of source data that the image contains. If + * called early on, this can help reduce copying and reallocations by + * appropriately preallocating the source data buffer. + * + * We take this approach rather than having the source data management code do + * something more complicated (like chunklisting) because HTTP is by far the + * dominant source of images, and the Content-Length header is quite reliable. + * Thus, pre-allocation simplifies code and reduces the total number of + * allocations. + */ + nsresult SetSourceSizeHint(uint32_t aSizeHint); + + nsCString GetURIString() { + nsCString spec; + if (GetURI()) { + GetURI()->GetSpec(spec); + } + return spec; + } + + private: + nsresult Init(const char* aMimeType, uint32_t aFlags); + + /** + * Tries to retrieve a surface for this image with size @aSize, surface flags + * matching @aFlags, and a playback type of @aPlaybackType. + * + * If @aFlags specifies FLAG_SYNC_DECODE and we already have all the image + * data, we'll attempt a sync decode if no matching surface is found. If + * FLAG_SYNC_DECODE was not specified and no matching surface was found, we'll + * kick off an async decode so that the surface is (hopefully) available next + * time it's requested. aMarkUsed determines if we mark the surface used in + * the surface cache or not. + * + * @return a drawable surface, which may be empty if the requested surface + * could not be found. + */ + LookupResult LookupFrame(const OrientedIntSize& aSize, uint32_t aFlags, + PlaybackType aPlaybackType, bool aMarkUsed); + + /// Helper method for LookupFrame(). + LookupResult LookupFrameInternal(const OrientedIntSize& aSize, + uint32_t aFlags, PlaybackType aPlaybackType, + bool aMarkUsed); + + ImgDrawResult DrawInternal(DrawableSurface&& aFrameRef, gfxContext* aContext, + const OrientedIntSize& aSize, + const ImageRegion& aRegion, + gfx::SamplingFilter aSamplingFilter, + uint32_t aFlags, float aOpacity); + + ////////////////////////////////////////////////////////////////////////////// + // Decoding. + ////////////////////////////////////////////////////////////////////////////// + + /** + * Creates and runs a decoder, either synchronously or asynchronously + * according to @aFlags. Decodes at the provided target size @aSize, using + * decode flags @aFlags. Performs a single-frame decode of this image unless + * we know the image is animated *and* @aPlaybackType is + * PlaybackType::eAnimated. + * + * It's an error to call Decode() before this image's intrinsic size is + * available. A metadata decode must successfully complete first. + * + * aOutRanSync is set to true if the decode was run synchronously. + * aOutFailed is set to true if failed to start a decode. + */ + void Decode(const OrientedIntSize& aSize, uint32_t aFlags, + PlaybackType aPlaybackType, bool& aOutRanSync, bool& aOutFailed); + + /** + * Creates and runs a metadata decoder, either synchronously or + * asynchronously according to @aFlags. + */ + NS_IMETHOD DecodeMetadata(uint32_t aFlags); + + /** + * Sets the size, inherent orientation, animation metadata, and other + * information about the image gathered during decoding. + * + * This function may be called multiple times, but will throw an error if + * subsequent calls do not match the first. + * + * @param aMetadata The metadata to set on this image. + * @param aFromMetadataDecode True if this metadata came from a metadata + * decode; false if it came from a full decode. + * @return |true| unless a catastrophic failure was discovered. If |false| is + * returned, it indicates that the image is corrupt in a way that requires all + * surfaces to be discarded to recover. + */ + bool SetMetadata(const ImageMetadata& aMetadata, bool aFromMetadataDecode); + + /** + * In catastrophic circumstances like a GPU driver crash, the contents of our + * frames may become invalid. If the information we gathered during the + * metadata decode proves to be wrong due to image corruption, the frames we + * have may violate this class's invariants. Either way, we need to + * immediately discard the invalid frames and redecode so that callers don't + * perceive that we've entered an invalid state. + * + * RecoverFromInvalidFrames discards all existing frames and redecodes using + * the provided @aSize and @aFlags. + */ + void RecoverFromInvalidFrames(const OrientedIntSize& aSize, uint32_t aFlags); + + void OnSurfaceDiscardedInternal(bool aAnimatedFramesDiscarded); + + private: // data + OrientedIntSize mSize; + nsTArray mNativeSizes; + + // The orientation required to correctly orient the image, from the image's + // metadata. RasterImage will handle and apply this orientation itself. + Orientation mOrientation; + + // The resolution as specified in the image metadata, in dppx. + Resolution mResolution; + + /// If this has a value, we're waiting for SetSize() to send the load event. + Maybe mLoadProgress; + + // Hotspot of this image, or (0, 0) if there is no hotspot data. + // + // We assume (and assert) that no image has both orientation metadata and a + // hotspot, so we store this as an untyped point. + gfx::IntPoint mHotspot; + + /// If this image is animated, a FrameAnimator which manages its animation. + UniquePtr mFrameAnimator; + + /// Animation timeline and other state for animation images. + Maybe mAnimationState; + + // Image locking. + uint32_t mLockCount; + + // The type of decoder this image needs. Computed from the MIME type in + // Init(). + DecoderType mDecoderType; + + // How many times we've decoded this image. + // This is currently only used for statistics + int32_t mDecodeCount; + +#ifdef DEBUG + uint32_t mFramesNotified; +#endif + + // The source data for this image. + NotNull> mSourceBuffer; + + // Boolean flags (clustered together to conserve space): + MOZ_ATOMIC_BITFIELDS( + mAtomicBitfields, 16, + ((bool, HasSize, 1), // Has SetSize() been called? + (bool, Transient, 1), // Is the image short-lived? + (bool, SyncLoad, 1), // Are we loading synchronously? + (bool, Discardable, 1), // Is container discardable? + (bool, SomeSourceData, 1), // Do we have some source data? + (bool, AllSourceData, 1), // Do we have all the source data? + (bool, HasBeenDecoded, 1), // Decoded at least once? + + // Whether we're waiting to start animation. If we get a StartAnimation() + // call but we don't yet have more than one frame, mPendingAnimation is + // set so that we know to start animation later if/when we have more + // frames. + (bool, PendingAnimation, 1), + + // Whether the animation can stop, due to running out + // of frames, or no more owning request + (bool, AnimationFinished, 1), + + // Whether, once we are done doing a metadata decode, we should + // immediately kick off a full decode. + (bool, WantFullDecode, 1))) + + TimeStamp mDrawStartTime; + + // This field is set according to the DecoderType of this image once when + // initialized so that a decoder's flags can be set according to any + // preferences that affect its behavior in a way that would otherwise cause + // errors, such as enabling or disabling animation. + DecoderFlags mDefaultDecoderFlags = DefaultDecoderFlags(); + + ////////////////////////////////////////////////////////////////////////////// + // Scaling. + ////////////////////////////////////////////////////////////////////////////// + + // Determines whether we can downscale during decode with the given + // parameters. + bool CanDownscaleDuringDecode(const OrientedIntSize& aSize, uint32_t aFlags); + + // Error handling. + void DoError(); + + class HandleErrorWorker : public Runnable { + public: + /** + * Called from decoder threads when DoError() is called, since errors can't + * be handled safely off-main-thread. Dispatches an event which reinvokes + * DoError on the main thread if there isn't one already pending. + */ + static void DispatchIfNeeded(RasterImage* aImage); + + NS_IMETHOD Run() override; + + private: + explicit HandleErrorWorker(RasterImage* aImage); + + RefPtr mImage; + }; + + // Helpers + bool CanDiscard(); + + bool IsOpaque(); + + LookupResult RequestDecodeForSizeInternal(const OrientedIntSize& aSize, + uint32_t aFlags, + uint32_t aWhichFrame); + + protected: + explicit RasterImage(nsIURI* aURI = nullptr); + + bool ShouldAnimate() override; + + friend class ImageFactory; +}; + +inline NS_IMETHODIMP RasterImage::GetAnimationMode(uint16_t* aAnimationMode) { + return GetAnimationModeInternal(aAnimationMode); +} + +} // namespace image +} // namespace mozilla + +/** + * Casting RasterImage to nsISupports is ambiguous. This method handles that. + */ +inline nsISupports* ToSupports(mozilla::image::RasterImage* p) { + return NS_ISUPPORTS_CAST(mozilla::image::ImageResource*, p); +} + +#endif /* mozilla_image_RasterImage_h */ diff --git a/image/Resolution.h b/image/Resolution.h new file mode 100644 index 0000000000..b305b867b2 --- /dev/null +++ b/image/Resolution.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_Resolution_h +#define mozilla_image_Resolution_h + +#include "mozilla/Assertions.h" +#include + +namespace mozilla { +namespace image { + +/** + * The resolution of an image, in dppx. + */ +struct Resolution { + Resolution() = default; + Resolution(float aX, float aY) : mX(aX), mY(aY) { + MOZ_ASSERT(mX != 0.0f); + MOZ_ASSERT(mY != 0.0f); + } + + bool operator==(const Resolution& aOther) const { + return mX == aOther.mX && mY == aOther.mY; + } + bool operator!=(const Resolution& aOther) const { return !(*this == aOther); } + + float mX = 1.0f; + float mY = 1.0f; + + void ScaleBy(float aScale) { + if (MOZ_LIKELY(aScale != 0.0f)) { + mX *= aScale; + mY *= aScale; + } + } + + void ApplyXTo(int32_t& aWidth) const { + if (mX != 1.0f) { + aWidth = std::round(float(aWidth) / mX); + } + } + + void ApplyXTo(float& aWidth) const { + if (mX != 1.0f) { + aWidth /= mX; + } + } + + void ApplyYTo(int32_t& aHeight) const { + if (mY != 1.0f) { + aHeight = std::round(float(aHeight) / mY); + } + } + + void ApplyYTo(float& aHeight) const { + if (mY != 1.0f) { + aHeight /= mY; + } + } + + void ApplyTo(int32_t& aWidth, int32_t& aHeight) const { + ApplyXTo(aWidth); + ApplyYTo(aHeight); + } + + void ApplyTo(float& aWidth, float& aHeight) const { + ApplyXTo(aWidth); + ApplyYTo(aHeight); + } + + void ApplyInverseTo(int32_t& aWidth, int32_t& aHeight) { + if (mX != 1.0f) { + aWidth = std::round(float(aWidth) * mX); + } + if (mY != 1.0f) { + aHeight = std::round(float(aHeight) * mY); + } + } +}; + +} // namespace image + +using ImageResolution = image::Resolution; + +} // namespace mozilla + +#endif diff --git a/image/SVGDocumentWrapper.cpp b/image/SVGDocumentWrapper.cpp new file mode 100644 index 0000000000..2c081afb9e --- /dev/null +++ b/image/SVGDocumentWrapper.cpp @@ -0,0 +1,401 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SVGDocumentWrapper.h" + +#include "mozilla/PresShell.h" +#include "mozilla/SMILAnimationController.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentTimeline.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ImageTracker.h" +#include "mozilla/dom/SVGDocument.h" +#include "mozilla/dom/SVGSVGElement.h" +#include "nsICategoryManager.h" +#include "nsIChannel.h" +#include "nsIContentViewer.h" +#include "nsIDocumentLoaderFactory.h" +#include "nsIHttpChannel.h" +#include "nsIObserverService.h" +#include "nsIParser.h" +#include "nsIRequest.h" +#include "nsIStreamListener.h" +#include "nsIXMLContentSink.h" +#include "nsNetCID.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMimeTypes.h" +#include "nsRefreshDriver.h" + +namespace mozilla { + +using namespace dom; +using namespace gfx; + +namespace image { + +NS_IMPL_ISUPPORTS(SVGDocumentWrapper, nsIStreamListener, nsIRequestObserver, + nsIObserver, nsISupportsWeakReference) + +SVGDocumentWrapper::SVGDocumentWrapper() + : mIgnoreInvalidation(false), + mRegisteredForXPCOMShutdown(false), + mIsDrawing(false) {} + +SVGDocumentWrapper::~SVGDocumentWrapper() { + DestroyViewer(); + if (mRegisteredForXPCOMShutdown) { + UnregisterForXPCOMShutdown(); + } +} + +void SVGDocumentWrapper::DestroyViewer() { + if (mViewer) { + mViewer->GetDocument()->OnPageHide(false, nullptr); + mViewer->Close(nullptr); + mViewer->Destroy(); + mViewer = nullptr; + } +} + +nsIFrame* SVGDocumentWrapper::GetRootLayoutFrame() { + Element* rootElem = GetRootSVGElem(); + return rootElem ? rootElem->GetPrimaryFrame() : nullptr; +} + +void SVGDocumentWrapper::UpdateViewportBounds(const nsIntSize& aViewportSize) { + MOZ_ASSERT(!mIgnoreInvalidation, "shouldn't be reentrant"); + mIgnoreInvalidation = true; + + nsIntRect currentBounds; + mViewer->GetBounds(currentBounds); + + // If the bounds have changed, we need to do a layout flush. + if (currentBounds.Size() != aViewportSize) { + mViewer->SetBounds(IntRect(IntPoint(0, 0), aViewportSize)); + FlushLayout(); + } + + mIgnoreInvalidation = false; +} + +void SVGDocumentWrapper::FlushImageTransformInvalidation() { + MOZ_ASSERT(!mIgnoreInvalidation, "shouldn't be reentrant"); + + SVGSVGElement* svgElem = GetRootSVGElem(); + if (!svgElem) { + return; + } + + mIgnoreInvalidation = true; + svgElem->FlushImageTransformInvalidation(); + FlushLayout(); + mIgnoreInvalidation = false; +} + +bool SVGDocumentWrapper::IsAnimated() { + // Can be called for animated images during shutdown, after we've + // already Observe()'d XPCOM shutdown and cleared out our mViewer pointer. + if (!mViewer) { + return false; + } + + Document* doc = mViewer->GetDocument(); + if (!doc) { + return false; + } + if (doc->Timeline()->HasAnimations()) { + // CSS animations (technically HasAnimations() also checks for CSS + // transitions and Web animations but since SVG-as-an-image doesn't run + // script they will never run in the document that we wrap). + return true; + } + if (doc->HasAnimationController() && + doc->GetAnimationController()->HasRegisteredAnimations()) { + // SMIL animations + return true; + } + return false; +} + +void SVGDocumentWrapper::StartAnimation() { + // Can be called for animated images during shutdown, after we've + // already Observe()'d XPCOM shutdown and cleared out our mViewer pointer. + if (!mViewer) { + return; + } + + Document* doc = mViewer->GetDocument(); + if (doc) { + SMILAnimationController* controller = doc->GetAnimationController(); + if (controller) { + controller->Resume(SMILTimeContainer::PAUSE_IMAGE); + } + doc->ImageTracker()->SetAnimatingState(true); + } +} + +void SVGDocumentWrapper::StopAnimation() { + // Can be called for animated images during shutdown, after we've + // already Observe()'d XPCOM shutdown and cleared out our mViewer pointer. + if (!mViewer) { + return; + } + + Document* doc = mViewer->GetDocument(); + if (doc) { + SMILAnimationController* controller = doc->GetAnimationController(); + if (controller) { + controller->Pause(SMILTimeContainer::PAUSE_IMAGE); + } + doc->ImageTracker()->SetAnimatingState(false); + } +} + +void SVGDocumentWrapper::ResetAnimation() { + SVGSVGElement* svgElem = GetRootSVGElem(); + if (!svgElem) { + return; + } + + svgElem->SetCurrentTime(0.0f); +} + +float SVGDocumentWrapper::GetCurrentTimeAsFloat() { + SVGSVGElement* svgElem = GetRootSVGElem(); + return svgElem ? svgElem->GetCurrentTimeAsFloat() : 0.0f; +} + +void SVGDocumentWrapper::SetCurrentTime(float aTime) { + SVGSVGElement* svgElem = GetRootSVGElem(); + if (svgElem && svgElem->GetCurrentTimeAsFloat() != aTime) { + svgElem->SetCurrentTime(aTime); + } +} + +void SVGDocumentWrapper::TickRefreshDriver() { + if (RefPtr presShell = mViewer->GetPresShell()) { + if (RefPtr presContext = presShell->GetPresContext()) { + if (RefPtr driver = presContext->RefreshDriver()) { + driver->DoTick(); + } + } + } +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +SVGDocumentWrapper::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + return mListener->OnDataAvailable(aRequest, inStr, sourceOffset, count); +} + +/** nsIRequestObserver methods **/ + +NS_IMETHODIMP +SVGDocumentWrapper::OnStartRequest(nsIRequest* aRequest) { + nsresult rv = SetupViewer(aRequest, getter_AddRefs(mViewer), + getter_AddRefs(mLoadGroup)); + + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(mListener->OnStartRequest(aRequest))) { + mViewer->GetDocument()->SetIsBeingUsedAsImage(); + StopAnimation(); // otherwise animations start automatically in helper doc + + rv = mViewer->Init(nullptr, nsIntRect(0, 0, 0, 0), nullptr); + if (NS_SUCCEEDED(rv)) { + rv = mViewer->Open(nullptr, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP +SVGDocumentWrapper::OnStopRequest(nsIRequest* aRequest, nsresult status) { + if (mListener) { + mListener->OnStopRequest(aRequest, status); + mListener = nullptr; + } + + return NS_OK; +} + +/** nsIObserver Methods **/ +NS_IMETHODIMP +SVGDocumentWrapper::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + // Sever ties from rendering observers to helper-doc's root SVG node + SVGSVGElement* svgElem = GetRootSVGElem(); + if (svgElem) { + SVGObserverUtils::RemoveAllRenderingObservers(svgElem); + } + + // Clean up at XPCOM shutdown time. + DestroyViewer(); + if (mListener) { + mListener = nullptr; + } + if (mLoadGroup) { + mLoadGroup = nullptr; + } + + // Turn off "registered" flag, or else we'll try to unregister when we die. + // (No need for that now, and the try would fail anyway -- it's too late.) + mRegisteredForXPCOMShutdown = false; + } else { + NS_ERROR("Unexpected observer topic."); + } + return NS_OK; +} + +/** Private helper methods **/ + +// This method is largely cribbed from +// nsExternalResourceMap::PendingLoad::SetupViewer. +nsresult SVGDocumentWrapper::SetupViewer(nsIRequest* aRequest, + nsIContentViewer** aViewer, + nsILoadGroup** aLoadGroup) { + nsCOMPtr chan(do_QueryInterface(aRequest)); + NS_ENSURE_TRUE(chan, NS_ERROR_UNEXPECTED); + + // Check for HTTP error page + nsCOMPtr httpChannel(do_QueryInterface(aRequest)); + if (httpChannel) { + bool requestSucceeded; + if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) || + !requestSucceeded) { + return NS_ERROR_FAILURE; + } + } + + // Give this document its own loadgroup + nsCOMPtr loadGroup; + chan->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr newLoadGroup = + do_CreateInstance(NS_LOADGROUP_CONTRACTID); + NS_ENSURE_TRUE(newLoadGroup, NS_ERROR_OUT_OF_MEMORY); + newLoadGroup->SetLoadGroup(loadGroup); + + nsCOMPtr catMan = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + NS_ENSURE_TRUE(catMan, NS_ERROR_NOT_AVAILABLE); + nsCString contractId; + nsresult rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", IMAGE_SVG_XML, + contractId); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr docLoaderFactory = + do_GetService(contractId.get()); + NS_ENSURE_TRUE(docLoaderFactory, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr viewer; + nsCOMPtr listener; + rv = docLoaderFactory->CreateInstance( + "external-resource", chan, newLoadGroup, nsLiteralCString(IMAGE_SVG_XML), + nullptr, nullptr, getter_AddRefs(listener), getter_AddRefs(viewer)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(viewer, NS_ERROR_UNEXPECTED); + + // Create a navigation time object and pass it to the SVG document through + // the viewer. + // The timeline(DocumentTimeline, used in CSS animation) of this SVG + // document needs this navigation timing object for time computation, such + // as to calculate current time stamp based on the start time of navigation + // time object. + // + // For a root document, DocShell would do these sort of things + // automatically. Since there is no DocShell for this wrapped SVG document, + // we must set it up manually. + RefPtr timing = new nsDOMNavigationTiming(nullptr); + timing->NotifyNavigationStart( + nsDOMNavigationTiming::DocShellState::eInactive); + viewer->SetNavigationTiming(timing); + + nsCOMPtr parser = do_QueryInterface(listener); + NS_ENSURE_TRUE(parser, NS_ERROR_UNEXPECTED); + + // XML-only, because this is for SVG content + nsCOMPtr sink = parser->GetContentSink(); + NS_ENSURE_TRUE(sink, NS_ERROR_UNEXPECTED); + + listener.swap(mListener); + viewer.forget(aViewer); + newLoadGroup.forget(aLoadGroup); + + RegisterForXPCOMShutdown(); + return NS_OK; +} + +void SVGDocumentWrapper::RegisterForXPCOMShutdown() { + MOZ_ASSERT(!mRegisteredForXPCOMShutdown, "re-registering for XPCOM shutdown"); + // Listen for xpcom-shutdown so that we can drop references to our + // helper-document at that point. (Otherwise, we won't get cleaned up + // until imgLoader::Shutdown, which can happen after the JAR service + // and RDF service have been unregistered.) + nsresult rv; + nsCOMPtr obsSvc = do_GetService(OBSERVER_SVC_CID, &rv); + if (NS_FAILED(rv) || NS_FAILED(obsSvc->AddObserver( + this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true))) { + NS_WARNING("Failed to register as observer of XPCOM shutdown"); + } else { + mRegisteredForXPCOMShutdown = true; + } +} + +void SVGDocumentWrapper::UnregisterForXPCOMShutdown() { + MOZ_ASSERT(mRegisteredForXPCOMShutdown, + "unregistering for XPCOM shutdown w/out being registered"); + + nsresult rv; + nsCOMPtr obsSvc = do_GetService(OBSERVER_SVC_CID, &rv); + if (NS_FAILED(rv) || + NS_FAILED(obsSvc->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID))) { + NS_WARNING("Failed to unregister as observer of XPCOM shutdown"); + } else { + mRegisteredForXPCOMShutdown = false; + } +} + +void SVGDocumentWrapper::FlushLayout() { + if (SVGDocument* doc = GetDocument()) { + doc->FlushPendingNotifications(FlushType::Layout); + } +} + +SVGDocument* SVGDocumentWrapper::GetDocument() { + if (!mViewer) { + return nullptr; + } + Document* doc = mViewer->GetDocument(); + if (!doc) { + return nullptr; + } + return doc->AsSVGDocument(); +} + +SVGSVGElement* SVGDocumentWrapper::GetRootSVGElem() { + if (!mViewer) { + return nullptr; // Can happen during destruction + } + + Document* doc = mViewer->GetDocument(); + if (!doc) { + return nullptr; // Can happen during destruction + } + + Element* rootElem = mViewer->GetDocument()->GetRootElement(); + if (!rootElem || !rootElem->IsSVGElement(nsGkAtoms::svg)) { + return nullptr; + } + + return static_cast(rootElem); +} + +} // namespace image +} // namespace mozilla diff --git a/image/SVGDocumentWrapper.h b/image/SVGDocumentWrapper.h new file mode 100644 index 0000000000..75924a4ea5 --- /dev/null +++ b/image/SVGDocumentWrapper.h @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This class wraps an SVG document, for use by VectorImage objects. */ + +#ifndef mozilla_image_SVGDocumentWrapper_h +#define mozilla_image_SVGDocumentWrapper_h + +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" +#include "nsIStreamListener.h" +#include "nsIObserver.h" +#include "nsIContentViewer.h" +#include "nsWeakReference.h" +#include "nsSize.h" + +class nsIRequest; +class nsILoadGroup; +class nsIFrame; + +#define OBSERVER_SVC_CID "@mozilla.org/observer-service;1" + +namespace mozilla { +class PresShell; +namespace dom { +class SVGSVGElement; +class SVGDocument; +} // namespace dom + +namespace image { +class AutoRestoreSVGState; + +class SVGDocumentWrapper final : public nsIStreamListener, + public nsIObserver, + public nsSupportsWeakReference { + public: + SVGDocumentWrapper(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIOBSERVER + + enum Dimension { eWidth, eHeight }; + + /** + * Returns the wrapped document, or nullptr on failure. (No AddRef.) + */ + mozilla::dom::SVGDocument* GetDocument(); + + /** + * Returns the root element for the wrapped document, or nullptr on + * failure. + */ + mozilla::dom::SVGSVGElement* GetRootSVGElem(); + + /** + * Returns the root nsIFrame* for the wrapped document, or nullptr on failure. + * + * @return the root nsIFrame* for the wrapped document, or nullptr on failure. + */ + nsIFrame* GetRootLayoutFrame(); + + /** + * Returns the mozilla::PresShell for the wrapped document. + */ + inline mozilla::PresShell* GetPresShell() { return mViewer->GetPresShell(); } + + /** + * Modifier to update the viewport dimensions of the wrapped document. This + * method performs a synchronous "FlushType::Layout" on the wrapped document, + * since a viewport-change affects layout. + * + * @param aViewportSize The new viewport dimensions. + */ + void UpdateViewportBounds(const nsIntSize& aViewportSize); + + /** + * If an SVG image's helper document has a pending notification for an + * override on the root node's "preserveAspectRatio" attribute, then this + * method will flush that notification so that the image can paint correctly. + * (First, though, it sets the mIgnoreInvalidation flag so that we won't + * notify the image's observers and trigger unwanted repaint-requests.) + */ + void FlushImageTransformInvalidation(); + + /** + * Returns a bool indicating whether the document has any SMIL animations. + * + * @return true if the document has any SMIL animations. Else, false. + */ + bool IsAnimated(); + + /** + * Indicates whether we should currently ignore rendering invalidations sent + * from the wrapped SVG doc. + * + * @return true if we should ignore invalidations sent from this SVG doc. + */ + bool ShouldIgnoreInvalidation() { return mIgnoreInvalidation; } + + /** + * Returns a bool indicating whether the document is currently drawing. + * + * @return true if the document is drawing. Else, false. + */ + bool IsDrawing() const { return mIsDrawing; } + + /** + * Methods to control animation. + */ + void StartAnimation(); + void StopAnimation(); + void ResetAnimation(); + float GetCurrentTimeAsFloat(); + void SetCurrentTime(float aTime); + void TickRefreshDriver(); + + /** + * Force a layout flush of the underlying SVG document. + */ + void FlushLayout(); + + private: + friend class AutoRestoreSVGState; + + ~SVGDocumentWrapper(); + + nsresult SetupViewer(nsIRequest* aRequest, nsIContentViewer** aViewer, + nsILoadGroup** aLoadGroup); + void DestroyViewer(); + void RegisterForXPCOMShutdown(); + void UnregisterForXPCOMShutdown(); + + nsCOMPtr mViewer; + nsCOMPtr mLoadGroup; + nsCOMPtr mListener; + bool mIgnoreInvalidation; + bool mRegisteredForXPCOMShutdown; + bool mIsDrawing; +}; + +} // namespace image +} // namespace mozilla + +/** + * Casting SVGDocumentWrapper to nsISupports is ambiguous. This method handles + * that. + */ +inline nsISupports* ToSupports(mozilla::image::SVGDocumentWrapper* p) { + return NS_ISUPPORTS_CAST(nsSupportsWeakReference*, p); +} + +#endif // mozilla_image_SVGDocumentWrapper_h diff --git a/image/SVGDrawingCallback.h b/image/SVGDrawingCallback.h new file mode 100644 index 0000000000..ea06fbc64f --- /dev/null +++ b/image/SVGDrawingCallback.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_SVGDrawingCallback_h +#define mozilla_image_SVGDrawingCallback_h + +#include "gfxDrawable.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/gfx/Types.h" + +namespace mozilla { +namespace image { +class SVGDocumentWrapper; + +class SVGDrawingCallback final : public gfxDrawingCallback { + public: + SVGDrawingCallback(SVGDocumentWrapper* aSVGDocumentWrapper, + const gfx::IntSize& aViewportSize, + const gfx::IntSize& aSize, uint32_t aImageFlags); + + ~SVGDrawingCallback(); + + bool operator()(gfxContext* aContext, const gfxRect& aFillRect, + const gfx::SamplingFilter aSamplingFilter, + const gfxMatrix& aTransform) override; + + private: + RefPtr mSVGDocumentWrapper; + const gfx::IntSize mViewportSize; + const gfx::IntSize mSize; + uint32_t mImageFlags; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SVGDrawingCallback_h diff --git a/image/SVGDrawingParameters.h b/image/SVGDrawingParameters.h new file mode 100644 index 0000000000..fa98162139 --- /dev/null +++ b/image/SVGDrawingParameters.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_SVGDrawingParameters_h +#define mozilla_image_SVGDrawingParameters_h + +#include "gfxContext.h" +#include "gfxTypes.h" +#include "ImageRegion.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/Maybe.h" +#include "mozilla/SVGImageContext.h" +#include "nsSize.h" + +namespace mozilla { +namespace image { + +struct SVGDrawingParameters { + typedef mozilla::gfx::IntSize IntSize; + typedef mozilla::gfx::SamplingFilter SamplingFilter; + + SVGDrawingParameters(gfxContext* aContext, const nsIntSize& aRasterSize, + const nsIntSize& aDrawSize, const ImageRegion& aRegion, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, float aAnimationTime, + uint32_t aFlags, float aOpacity) + : context(aContext), + size(aRasterSize), + drawSize(aDrawSize), + region(aRegion), + samplingFilter(aSamplingFilter), + svgContext(aSVGContext), + viewportSize(aRasterSize), + animationTime(aAnimationTime), + flags(aFlags), + opacity(aOpacity) { + if (auto sz = aSVGContext.GetViewportSize()) { + viewportSize = nsIntSize(sz->width, sz->height); // XXX losing unit + } + } + + gfxContext* context; + IntSize size; // Size to rasterize a surface at. + IntSize drawSize; // Size to draw the given surface at. + ImageRegion region; + SamplingFilter samplingFilter; + const SVGImageContext& svgContext; + nsIntSize viewportSize; + float animationTime; + uint32_t flags; + gfxFloat opacity; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SVGDrawingParameters_h diff --git a/image/ScriptedNotificationObserver.cpp b/image/ScriptedNotificationObserver.cpp new file mode 100644 index 0000000000..4797b182c3 --- /dev/null +++ b/image/ScriptedNotificationObserver.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScriptedNotificationObserver.h" +#include "imgIScriptedNotificationObserver.h" +#include "nsCycleCollectionParticipant.h" +#include "nsContentUtils.h" // for nsAutoScriptBlocker + +namespace mozilla { +namespace image { + +NS_IMPL_CYCLE_COLLECTION(ScriptedNotificationObserver, mInner) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptedNotificationObserver) + NS_INTERFACE_MAP_ENTRY(imgINotificationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptedNotificationObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptedNotificationObserver) + +ScriptedNotificationObserver::ScriptedNotificationObserver( + imgIScriptedNotificationObserver* aInner) + : mInner(aInner) {} + +void ScriptedNotificationObserver::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* /*aUnused*/) { + // For now, we block (other) scripts from running to preserve the historical + // behavior from when ScriptedNotificationObserver::Notify was called as part + // of the observers list in nsImageLoadingContent::Notify. Now each + // ScriptedNotificationObserver has its own imgRequestProxy and thus gets + // Notify called directly by imagelib. + nsAutoScriptBlocker scriptBlocker; + + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + mInner->SizeAvailable(aRequest); + return; + } + if (aType == imgINotificationObserver::FRAME_UPDATE) { + mInner->FrameUpdate(aRequest); + return; + } + if (aType == imgINotificationObserver::FRAME_COMPLETE) { + mInner->FrameComplete(aRequest); + return; + } + if (aType == imgINotificationObserver::DECODE_COMPLETE) { + mInner->DecodeComplete(aRequest); + return; + } + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + mInner->LoadComplete(aRequest); + return; + } + if (aType == imgINotificationObserver::DISCARD) { + mInner->Discard(aRequest); + return; + } + if (aType == imgINotificationObserver::IS_ANIMATED) { + mInner->IsAnimated(aRequest); + return; + } + if (aType == imgINotificationObserver::HAS_TRANSPARENCY) { + mInner->HasTransparency(aRequest); + return; + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/ScriptedNotificationObserver.h b/image/ScriptedNotificationObserver.h new file mode 100644 index 0000000000..ceeafa532f --- /dev/null +++ b/image/ScriptedNotificationObserver.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_ScriptedNotificationObserver_h +#define mozilla_image_ScriptedNotificationObserver_h + +#include "imgINotificationObserver.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" + +class imgIScriptedNotificationObserver; + +namespace mozilla { +namespace image { + +class ScriptedNotificationObserver : public imgINotificationObserver { + public: + explicit ScriptedNotificationObserver( + imgIScriptedNotificationObserver* aInner); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + NS_DECL_CYCLE_COLLECTION_CLASS(ScriptedNotificationObserver) + + private: + virtual ~ScriptedNotificationObserver() {} + nsCOMPtr mInner; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ScriptedNotificationObserver_h diff --git a/image/ShutdownTracker.cpp b/image/ShutdownTracker.cpp new file mode 100644 index 0000000000..4eef66e52e --- /dev/null +++ b/image/ShutdownTracker.cpp @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ShutdownTracker.h" + +#include "mozilla/Services.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" + +namespace mozilla { +namespace image { + +class ShutdownTrackerImpl; + +/////////////////////////////////////////////////////////////////////////////// +// Static Data +/////////////////////////////////////////////////////////////////////////////// + +// Whether we've observed shutdown starting yet. +static bool sShutdownHasStarted = false; + +/////////////////////////////////////////////////////////////////////////////// +// Implementation +/////////////////////////////////////////////////////////////////////////////// + +struct ShutdownObserver : public nsIObserver { + NS_DECL_ISUPPORTS + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + if (strcmp(aTopic, "xpcom-will-shutdown") != 0) { + return NS_OK; + } + + nsCOMPtr os = services::GetObserverService(); + if (os) { + os->RemoveObserver(this, "xpcom-will-shutdown"); + } + + sShutdownHasStarted = true; + return NS_OK; + } + + private: + virtual ~ShutdownObserver() {} +}; + +NS_IMPL_ISUPPORTS(ShutdownObserver, nsIObserver) + +/////////////////////////////////////////////////////////////////////////////// +// Public API +/////////////////////////////////////////////////////////////////////////////// + +/* static */ +void ShutdownTracker::Initialize() { + nsCOMPtr os = services::GetObserverService(); + if (os) { + os->AddObserver(new ShutdownObserver, "xpcom-will-shutdown", false); + } +} + +/* static */ +bool ShutdownTracker::ShutdownHasStarted() { return sShutdownHasStarted; } + +} // namespace image +} // namespace mozilla diff --git a/image/ShutdownTracker.h b/image/ShutdownTracker.h new file mode 100644 index 0000000000..9e5237cc3d --- /dev/null +++ b/image/ShutdownTracker.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * ShutdownTracker is an imagelib-global service that allows callers to check + * whether shutdown has started. + */ + +#ifndef mozilla_image_ShutdownTracker_h +#define mozilla_image_ShutdownTracker_h + +namespace mozilla { +namespace image { + +/** + * ShutdownTracker is an imagelib-global service that allows callers to check + * whether shutdown has started. It exists to avoid the need for registering + * many 'xpcom-will-shutdown' notification observers on short-lived objects, + * which would have an unnecessary performance cost. + */ +struct ShutdownTracker { + /** + * Initialize static data. Called during imagelib module initialization. + */ + static void Initialize(); + + /** + * Check whether shutdown has started. Callers can use this to check whether + * it's safe to access XPCOM services; if shutdown has started, such calls + * must be avoided. + * + * @return true if shutdown has already started. + */ + static bool ShutdownHasStarted(); + + private: + virtual ~ShutdownTracker() = 0; // Forbid instantiation. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_ShutdownTracker_h diff --git a/image/SourceBuffer.cpp b/image/SourceBuffer.cpp new file mode 100644 index 0000000000..682ba84f8f --- /dev/null +++ b/image/SourceBuffer.cpp @@ -0,0 +1,706 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SourceBuffer.h" + +#include +#include +#include +#include "mozilla/Likely.h" +#include "nsIInputStream.h" +#include "MainThreadUtils.h" +#include "SurfaceCache.h" + +using std::max; +using std::min; + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////////////// +// SourceBufferIterator implementation. +////////////////////////////////////////////////////////////////////////////// + +SourceBufferIterator::~SourceBufferIterator() { + if (mOwner) { + mOwner->OnIteratorRelease(); + } +} + +SourceBufferIterator& SourceBufferIterator::operator=( + SourceBufferIterator&& aOther) { + if (mOwner) { + mOwner->OnIteratorRelease(); + } + + mOwner = std::move(aOther.mOwner); + mState = aOther.mState; + mData = aOther.mData; + mChunkCount = aOther.mChunkCount; + mByteCount = aOther.mByteCount; + mRemainderToRead = aOther.mRemainderToRead; + + return *this; +} + +SourceBufferIterator::State SourceBufferIterator::AdvanceOrScheduleResume( + size_t aRequestedBytes, IResumable* aConsumer) { + MOZ_ASSERT(mOwner); + + if (MOZ_UNLIKELY(!HasMore())) { + MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator"); + return COMPLETE; + } + + // The range of data [mOffset, mOffset + mNextReadLength) has just been read + // by the caller (or at least they don't have any interest in it), so consume + // that data. + MOZ_ASSERT(mData.mIterating.mNextReadLength <= + mData.mIterating.mAvailableLength); + mData.mIterating.mOffset += mData.mIterating.mNextReadLength; + mData.mIterating.mAvailableLength -= mData.mIterating.mNextReadLength; + + // An iterator can have a limit imposed on it to read only a subset of a + // source buffer. If it is present, we need to mimic the same behaviour as + // the owning SourceBuffer. + if (MOZ_UNLIKELY(mRemainderToRead != SIZE_MAX)) { + MOZ_ASSERT(mData.mIterating.mNextReadLength <= mRemainderToRead); + mRemainderToRead -= mData.mIterating.mNextReadLength; + + if (MOZ_UNLIKELY(mRemainderToRead == 0)) { + mData.mIterating.mNextReadLength = 0; + SetComplete(NS_OK); + return COMPLETE; + } + + if (MOZ_UNLIKELY(aRequestedBytes > mRemainderToRead)) { + aRequestedBytes = mRemainderToRead; + } + } + + mData.mIterating.mNextReadLength = 0; + + if (MOZ_LIKELY(mState == READY)) { + // If the caller wants zero bytes of data, that's easy enough; we just + // configured ourselves for a zero-byte read above! In theory we could do + // this even in the START state, but it's not important for performance and + // breaking the ability of callers to assert that the pointer returned by + // Data() is non-null doesn't seem worth it. + if (aRequestedBytes == 0) { + MOZ_ASSERT(mData.mIterating.mNextReadLength == 0); + return READY; + } + + // Try to satisfy the request out of our local buffer. This is potentially + // much faster than requesting data from our owning SourceBuffer because we + // don't have to take the lock. Note that if we have anything at all in our + // local buffer, we use it to satisfy the request; @aRequestedBytes is just + // the *maximum* number of bytes we can return. + if (mData.mIterating.mAvailableLength > 0) { + return AdvanceFromLocalBuffer(aRequestedBytes); + } + } + + // Our local buffer is empty, so we'll have to request data from our owning + // SourceBuffer. + return mOwner->AdvanceIteratorOrScheduleResume(*this, aRequestedBytes, + aConsumer); +} + +bool SourceBufferIterator::RemainingBytesIsNoMoreThan(size_t aBytes) const { + MOZ_ASSERT(mOwner); + return mOwner->RemainingBytesIsNoMoreThan(*this, aBytes); +} + +////////////////////////////////////////////////////////////////////////////// +// SourceBuffer implementation. +////////////////////////////////////////////////////////////////////////////// + +const size_t SourceBuffer::MIN_CHUNK_CAPACITY; +const size_t SourceBuffer::MAX_CHUNK_CAPACITY; + +SourceBuffer::SourceBuffer() + : mMutex("image::SourceBuffer"), mConsumerCount(0), mCompacted(false) {} + +SourceBuffer::~SourceBuffer() { + MOZ_ASSERT(mConsumerCount == 0, + "SourceBuffer destroyed with active consumers"); +} + +nsresult SourceBuffer::AppendChunk(Maybe&& aChunk) { + mMutex.AssertCurrentThreadOwns(); + +#ifdef DEBUG + if (mChunks.Length() > 0) { + NS_WARNING("Appending an extra chunk for SourceBuffer"); + } +#endif + + if (MOZ_UNLIKELY(!aChunk)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (MOZ_UNLIKELY(aChunk->AllocationFailed())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (MOZ_UNLIKELY(!mChunks.AppendElement(std::move(*aChunk), fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +Maybe SourceBuffer::CreateChunk( + size_t aCapacity, size_t aExistingCapacity /* = 0 */, + bool aRoundUp /* = true */) { + if (MOZ_UNLIKELY(aCapacity == 0)) { + MOZ_ASSERT_UNREACHABLE("Appending a chunk of zero size?"); + return Nothing(); + } + + // Round up if requested. + size_t finalCapacity = aRoundUp ? RoundedUpCapacity(aCapacity) : aCapacity; + + // Use the size of the SurfaceCache as an additional heuristic to avoid + // allocating huge buffers. Generally images do not get smaller when decoded, + // so if we could store the source data in the SurfaceCache, we assume that + // there's no way we'll be able to store the decoded version. + if (MOZ_UNLIKELY(!SurfaceCache::CanHold(finalCapacity + aExistingCapacity))) { + NS_WARNING( + "SourceBuffer refused to create chunk too large for SurfaceCache"); + return Nothing(); + } + + return Some(Chunk(finalCapacity)); +} + +nsresult SourceBuffer::Compact() { + mMutex.AssertCurrentThreadOwns(); + + MOZ_ASSERT(mConsumerCount == 0, "Should have no consumers here"); + MOZ_ASSERT(mWaitingConsumers.Length() == 0, "Shouldn't have waiters"); + MOZ_ASSERT(mStatus, "Should be complete here"); + + // If we've tried to compact once, don't attempt again. + if (mCompacted) { + return NS_OK; + } + + mCompacted = true; + + // Compact our waiting consumers list, since we're complete and no future + // consumer will ever have to wait. + mWaitingConsumers.Compact(); + + // If we have no chunks, then there's nothing to compact. + if (mChunks.Length() < 1) { + return NS_OK; + } + + // If we have one chunk, then we can compact if it has excess capacity. + if (mChunks.Length() == 1 && mChunks[0].Length() == mChunks[0].Capacity()) { + return NS_OK; + } + + // If the last chunk has the maximum capacity, then we know the total size + // will be quite large and not worth consolidating. We can likely/cheapily + // trim the last chunk if it is too big however. + size_t capacity = mChunks.LastElement().Capacity(); + if (capacity == MAX_CHUNK_CAPACITY) { + size_t lastLength = mChunks.LastElement().Length(); + if (lastLength != capacity) { + mChunks.LastElement().SetCapacity(lastLength); + } + return NS_OK; + } + + // We can compact our buffer. Determine the total length. + size_t length = 0; + for (uint32_t i = 0; i < mChunks.Length(); ++i) { + length += mChunks[i].Length(); + } + + // If our total length is zero (which means ExpectLength() got called, but no + // data ever actually got written) then just empty our chunk list. + if (MOZ_UNLIKELY(length == 0)) { + mChunks.Clear(); + return NS_OK; + } + + Chunk& mergeChunk = mChunks[0]; + if (MOZ_UNLIKELY(!mergeChunk.SetCapacity(length))) { + NS_WARNING("Failed to reallocate chunk for SourceBuffer compacting - OOM?"); + return NS_OK; + } + + // Copy our old chunks into the newly reallocated first chunk. + for (uint32_t i = 1; i < mChunks.Length(); ++i) { + size_t offset = mergeChunk.Length(); + MOZ_ASSERT(offset < mergeChunk.Capacity()); + MOZ_ASSERT(offset + mChunks[i].Length() <= mergeChunk.Capacity()); + + memcpy(mergeChunk.Data() + offset, mChunks[i].Data(), mChunks[i].Length()); + mergeChunk.AddLength(mChunks[i].Length()); + } + + MOZ_ASSERT(mergeChunk.Length() == mergeChunk.Capacity(), + "Compacted chunk has slack space"); + + // Remove the redundant chunks. + mChunks.RemoveLastElements(mChunks.Length() - 1); + mChunks.Compact(); + + return NS_OK; +} + +/* static */ +size_t SourceBuffer::RoundedUpCapacity(size_t aCapacity) { + // Protect against overflow. + if (MOZ_UNLIKELY(SIZE_MAX - aCapacity < MIN_CHUNK_CAPACITY)) { + return aCapacity; + } + + // Round up to the next multiple of MIN_CHUNK_CAPACITY (which should be the + // size of a page). + size_t roundedCapacity = + (aCapacity + MIN_CHUNK_CAPACITY - 1) & ~(MIN_CHUNK_CAPACITY - 1); + MOZ_ASSERT(roundedCapacity >= aCapacity, "Bad math?"); + MOZ_ASSERT(roundedCapacity - aCapacity < MIN_CHUNK_CAPACITY, "Bad math?"); + + return roundedCapacity; +} + +size_t SourceBuffer::FibonacciCapacityWithMinimum(size_t aMinCapacity) { + mMutex.AssertCurrentThreadOwns(); + + // We grow the source buffer using a Fibonacci growth rate. It will be capped + // at MAX_CHUNK_CAPACITY, unless the available data exceeds that. + + size_t length = mChunks.Length(); + + if (length == 0 || aMinCapacity > MAX_CHUNK_CAPACITY) { + return aMinCapacity; + } + + if (length == 1) { + return min(max(2 * mChunks[0].Capacity(), aMinCapacity), + MAX_CHUNK_CAPACITY); + } + + return min( + max(mChunks[length - 1].Capacity() + mChunks[length - 2].Capacity(), + aMinCapacity), + MAX_CHUNK_CAPACITY); +} + +void SourceBuffer::AddWaitingConsumer(IResumable* aConsumer) { + mMutex.AssertCurrentThreadOwns(); + + MOZ_ASSERT(!mStatus, "Waiting when we're complete?"); + + if (aConsumer) { + mWaitingConsumers.AppendElement(aConsumer); + } +} + +void SourceBuffer::ResumeWaitingConsumers() { + mMutex.AssertCurrentThreadOwns(); + + if (mWaitingConsumers.Length() == 0) { + return; + } + + for (uint32_t i = 0; i < mWaitingConsumers.Length(); ++i) { + mWaitingConsumers[i]->Resume(); + } + + mWaitingConsumers.Clear(); +} + +nsresult SourceBuffer::ExpectLength(size_t aExpectedLength) { + MOZ_ASSERT(aExpectedLength > 0, "Zero expected size?"); + + MutexAutoLock lock(mMutex); + + if (MOZ_UNLIKELY(mStatus)) { + MOZ_ASSERT_UNREACHABLE("ExpectLength after SourceBuffer is complete"); + return NS_OK; + } + + if (MOZ_UNLIKELY(mChunks.Length() > 0)) { + MOZ_ASSERT_UNREACHABLE("Duplicate or post-Append call to ExpectLength"); + return NS_OK; + } + + if (MOZ_UNLIKELY(!SurfaceCache::CanHold(aExpectedLength))) { + NS_WARNING("SourceBuffer refused to store too large buffer"); + return HandleError(NS_ERROR_INVALID_ARG); + } + + size_t length = min(aExpectedLength, MAX_CHUNK_CAPACITY); + if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(CreateChunk(length, + /* aExistingCapacity */ 0, + /* aRoundUp */ false))))) { + return HandleError(NS_ERROR_OUT_OF_MEMORY); + } + + return NS_OK; +} + +nsresult SourceBuffer::Append(const char* aData, size_t aLength) { + MOZ_ASSERT(aData, "Should have a buffer"); + MOZ_ASSERT(aLength > 0, "Writing a zero-sized chunk"); + + size_t currentChunkCapacity = 0; + size_t currentChunkLength = 0; + char* currentChunkData = nullptr; + size_t currentChunkRemaining = 0; + size_t forCurrentChunk = 0; + size_t forNextChunk = 0; + size_t nextChunkCapacity = 0; + size_t totalCapacity = 0; + + { + MutexAutoLock lock(mMutex); + + if (MOZ_UNLIKELY(mStatus)) { + // This SourceBuffer is already complete; ignore further data. + return NS_ERROR_FAILURE; + } + + if (MOZ_UNLIKELY(mChunks.Length() == 0)) { + if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(CreateChunk(aLength))))) { + return HandleError(NS_ERROR_OUT_OF_MEMORY); + } + } + + // Copy out the current chunk's information so we can release the lock. + // Note that this wouldn't be safe if multiple producers were allowed! + Chunk& currentChunk = mChunks.LastElement(); + currentChunkCapacity = currentChunk.Capacity(); + currentChunkLength = currentChunk.Length(); + currentChunkData = currentChunk.Data(); + + // Partition this data between the current chunk and the next chunk. + // (Because we always allocate a chunk big enough to fit everything passed + // to Append, we'll never need more than those two chunks to store + // everything.) + currentChunkRemaining = currentChunkCapacity - currentChunkLength; + forCurrentChunk = min(aLength, currentChunkRemaining); + forNextChunk = aLength - forCurrentChunk; + + // If we'll need another chunk, determine what its capacity should be while + // we still hold the lock. + nextChunkCapacity = + forNextChunk > 0 ? FibonacciCapacityWithMinimum(forNextChunk) : 0; + + for (uint32_t i = 0; i < mChunks.Length(); ++i) { + totalCapacity += mChunks[i].Capacity(); + } + } + + // Write everything we can fit into the current chunk. + MOZ_ASSERT(currentChunkLength + forCurrentChunk <= currentChunkCapacity); + memcpy(currentChunkData + currentChunkLength, aData, forCurrentChunk); + + // If there's something left, create a new chunk and write it there. + Maybe nextChunk; + if (forNextChunk > 0) { + MOZ_ASSERT(nextChunkCapacity >= forNextChunk, "Next chunk too small?"); + nextChunk = CreateChunk(nextChunkCapacity, totalCapacity); + if (MOZ_LIKELY(nextChunk && !nextChunk->AllocationFailed())) { + memcpy(nextChunk->Data(), aData + forCurrentChunk, forNextChunk); + nextChunk->AddLength(forNextChunk); + } + } + + // Update shared data structures. + { + MutexAutoLock lock(mMutex); + + // Update the length of the current chunk. + Chunk& currentChunk = mChunks.LastElement(); + MOZ_ASSERT(currentChunk.Data() == currentChunkData, "Multiple producers?"); + MOZ_ASSERT(currentChunk.Length() == currentChunkLength, + "Multiple producers?"); + + currentChunk.AddLength(forCurrentChunk); + + // If we created a new chunk, add it to the series. + if (forNextChunk > 0) { + if (MOZ_UNLIKELY(!nextChunk)) { + return HandleError(NS_ERROR_OUT_OF_MEMORY); + } + + if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(std::move(nextChunk))))) { + return HandleError(NS_ERROR_OUT_OF_MEMORY); + } + } + + // Resume any waiting readers now that there's new data. + ResumeWaitingConsumers(); + } + + return NS_OK; +} + +static nsresult AppendToSourceBuffer(nsIInputStream*, void* aClosure, + const char* aFromRawSegment, uint32_t, + uint32_t aCount, uint32_t* aWriteCount) { + SourceBuffer* sourceBuffer = static_cast(aClosure); + + // Copy the source data. Unless we hit OOM, we squelch the return value here, + // because returning an error means that ReadSegments stops reading data, and + // we want to ensure that we read everything we get. If we hit OOM then we + // return a failed status to the caller. + nsresult rv = sourceBuffer->Append(aFromRawSegment, aCount); + if (rv == NS_ERROR_OUT_OF_MEMORY) { + return rv; + } + + // Report that we wrote everything we got. + *aWriteCount = aCount; + + return NS_OK; +} + +nsresult SourceBuffer::AppendFromInputStream(nsIInputStream* aInputStream, + uint32_t aCount) { + uint32_t bytesRead; + nsresult rv = aInputStream->ReadSegments(AppendToSourceBuffer, this, aCount, + &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (bytesRead == 0) { + // The loading of the image has been canceled. + return NS_ERROR_FAILURE; + } + + if (bytesRead != aCount) { + // Only some of the given data was read. We may have failed in + // SourceBuffer::Append but ReadSegments swallowed the error. Otherwise the + // stream itself failed to yield the data. + MutexAutoLock lock(mMutex); + if (mStatus) { + MOZ_ASSERT(NS_FAILED(*mStatus)); + return *mStatus; + } + + MOZ_ASSERT_UNREACHABLE("AppendToSourceBuffer should consume everything"); + } + + return rv; +} + +void SourceBuffer::Complete(nsresult aStatus) { + MutexAutoLock lock(mMutex); + + // When an error occurs internally (e.g. due to an OOM), we save the status. + // This will indirectly trigger a failure higher up and that will call + // SourceBuffer::Complete. Since it doesn't necessarily know we are already + // complete, it is safe to ignore. + if (mStatus && (MOZ_UNLIKELY(NS_SUCCEEDED(*mStatus) || + aStatus != NS_IMAGELIB_ERROR_FAILURE))) { + MOZ_ASSERT_UNREACHABLE("Called Complete more than once"); + return; + } + + if (MOZ_UNLIKELY(NS_SUCCEEDED(aStatus) && IsEmpty())) { + // It's illegal to succeed without writing anything. + aStatus = NS_ERROR_FAILURE; + } + + mStatus = Some(aStatus); + + // Resume any waiting consumers now that we're complete. + ResumeWaitingConsumers(); + + // If we still have active consumers, just return. + if (mConsumerCount > 0) { + return; + } + + // Attempt to compact our buffer down to a single chunk. + Compact(); +} + +bool SourceBuffer::IsComplete() { + MutexAutoLock lock(mMutex); + return bool(mStatus); +} + +size_t SourceBuffer::SizeOfIncludingThisWithComputedFallback( + MallocSizeOf aMallocSizeOf) const { + MutexAutoLock lock(mMutex); + + size_t n = aMallocSizeOf(this); + n += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf); + + for (uint32_t i = 0; i < mChunks.Length(); ++i) { + size_t chunkSize = aMallocSizeOf(mChunks[i].Data()); + + if (chunkSize == 0) { + // We're on a platform where moz_malloc_size_of always returns 0. + chunkSize = mChunks[i].Capacity(); + } + + n += chunkSize; + } + + return n; +} + +SourceBufferIterator SourceBuffer::Iterator(size_t aReadLength) { + { + MutexAutoLock lock(mMutex); + mConsumerCount++; + } + + return SourceBufferIterator(this, aReadLength); +} + +void SourceBuffer::OnIteratorRelease() { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mConsumerCount > 0, "Consumer count doesn't add up"); + mConsumerCount--; + + // If we still have active consumers, or we're not complete yet, then return. + if (mConsumerCount > 0 || !mStatus) { + return; + } + + // Attempt to compact our buffer down to a single chunk. + Compact(); +} + +bool SourceBuffer::RemainingBytesIsNoMoreThan( + const SourceBufferIterator& aIterator, size_t aBytes) const { + MutexAutoLock lock(mMutex); + + // If we're not complete, we always say no. + if (!mStatus) { + return false; + } + + // If the iterator's at the end, the answer is trivial. + if (!aIterator.HasMore()) { + return true; + } + + uint32_t iteratorChunk = aIterator.mData.mIterating.mChunk; + size_t iteratorOffset = aIterator.mData.mIterating.mOffset; + size_t iteratorLength = aIterator.mData.mIterating.mAvailableLength; + + // Include the bytes the iterator is currently pointing to in the limit, so + // that the current chunk doesn't have to be a special case. + size_t bytes = aBytes + iteratorOffset + iteratorLength; + + // Count the length over all of our chunks, starting with the one that the + // iterator is currently pointing to. (This is O(N), but N is expected to be + // ~1, so it doesn't seem worth caching the length separately.) + size_t lengthSoFar = 0; + for (uint32_t i = iteratorChunk; i < mChunks.Length(); ++i) { + lengthSoFar += mChunks[i].Length(); + if (lengthSoFar > bytes) { + return false; + } + } + + return true; +} + +SourceBufferIterator::State SourceBuffer::AdvanceIteratorOrScheduleResume( + SourceBufferIterator& aIterator, size_t aRequestedBytes, + IResumable* aConsumer) { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(aIterator.HasMore(), + "Advancing a completed iterator and " + "AdvanceOrScheduleResume didn't catch it"); + + if (MOZ_UNLIKELY(mStatus && NS_FAILED(*mStatus))) { + // This SourceBuffer is complete due to an error; all reads fail. + return aIterator.SetComplete(*mStatus); + } + + if (MOZ_UNLIKELY(mChunks.Length() == 0)) { + // We haven't gotten an initial chunk yet. + AddWaitingConsumer(aConsumer); + return aIterator.SetWaiting(!!aConsumer); + } + + uint32_t iteratorChunkIdx = aIterator.mData.mIterating.mChunk; + MOZ_ASSERT(iteratorChunkIdx < mChunks.Length()); + + const Chunk& currentChunk = mChunks[iteratorChunkIdx]; + size_t iteratorEnd = aIterator.mData.mIterating.mOffset + + aIterator.mData.mIterating.mAvailableLength; + MOZ_ASSERT(iteratorEnd <= currentChunk.Length()); + MOZ_ASSERT(iteratorEnd <= currentChunk.Capacity()); + + if (iteratorEnd < currentChunk.Length()) { + // There's more data in the current chunk. + return aIterator.SetReady(iteratorChunkIdx, currentChunk.Data(), + iteratorEnd, currentChunk.Length() - iteratorEnd, + aRequestedBytes); + } + + if (iteratorEnd == currentChunk.Capacity() && + !IsLastChunk(iteratorChunkIdx)) { + // Advance to the next chunk. + const Chunk& nextChunk = mChunks[iteratorChunkIdx + 1]; + return aIterator.SetReady(iteratorChunkIdx + 1, nextChunk.Data(), 0, + nextChunk.Length(), aRequestedBytes); + } + + MOZ_ASSERT(IsLastChunk(iteratorChunkIdx), "Should've advanced"); + + if (mStatus) { + // There's no more data and this SourceBuffer completed successfully. + MOZ_ASSERT(NS_SUCCEEDED(*mStatus), "Handled failures earlier"); + return aIterator.SetComplete(*mStatus); + } + + // We're not complete, but there's no more data right now. Arrange to wake up + // the consumer when we get more data. + AddWaitingConsumer(aConsumer); + return aIterator.SetWaiting(!!aConsumer); +} + +nsresult SourceBuffer::HandleError(nsresult aError) { + MOZ_ASSERT(NS_FAILED(aError), "Should have an error here"); + MOZ_ASSERT(aError == NS_ERROR_OUT_OF_MEMORY || aError == NS_ERROR_INVALID_ARG, + "Unexpected error; may want to notify waiting readers, which " + "HandleError currently doesn't do"); + + mMutex.AssertCurrentThreadOwns(); + + NS_WARNING("SourceBuffer encountered an unrecoverable error"); + + // Record the error. + mStatus = Some(aError); + + // Drop our references to waiting readers. + mWaitingConsumers.Clear(); + + return *mStatus; +} + +bool SourceBuffer::IsEmpty() { + mMutex.AssertCurrentThreadOwns(); + return mChunks.Length() == 0 || mChunks[0].Length() == 0; +} + +bool SourceBuffer::IsLastChunk(uint32_t aChunk) { + mMutex.AssertCurrentThreadOwns(); + return aChunk + 1 == mChunks.Length(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/SourceBuffer.h b/image/SourceBuffer.h new file mode 100644 index 0000000000..79188fb882 --- /dev/null +++ b/image/SourceBuffer.h @@ -0,0 +1,499 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * SourceBuffer is a single producer, multiple consumer data structure used for + * storing image source (compressed) data. + */ + +#ifndef mozilla_image_sourcebuffer_h +#define mozilla_image_sourcebuffer_h + +#include +#include + +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefCounted.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +class nsIInputStream; + +namespace mozilla { +namespace image { + +class SourceBuffer; + +/** + * IResumable is an interface for classes that can schedule themselves to resume + * their work later. An implementation of IResumable generally should post a + * runnable to some event target which continues the work of the task. + */ +struct IResumable { + MOZ_DECLARE_REFCOUNTED_TYPENAME(IResumable) + + // Subclasses may or may not be XPCOM classes, so we just require that they + // implement AddRef and Release. + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void Resume() = 0; + + protected: + virtual ~IResumable() {} +}; + +/** + * SourceBufferIterator is a class that allows consumers of image source data to + * read the contents of a SourceBuffer sequentially. + * + * Consumers can advance through the SourceBuffer by calling + * AdvanceOrScheduleResume() repeatedly. After every advance, they should call + * check the return value, which will tell them the iterator's new state. + * + * If WAITING is returned, AdvanceOrScheduleResume() has arranged + * to call the consumer's Resume() method later, so the consumer should save its + * state if needed and stop running. + * + * If the iterator's new state is READY, then the consumer can call Data() and + * Length() to read new data from the SourceBuffer. + * + * Finally, in the COMPLETE state the consumer can call CompletionStatus() to + * get the status passed to SourceBuffer::Complete(). + */ +class SourceBufferIterator final { + public: + enum State { + START, // The iterator is at the beginning of the buffer. + READY, // The iterator is pointing to new data. + WAITING, // The iterator is blocked and the caller must yield. + COMPLETE // The iterator is pointing to the end of the buffer. + }; + + explicit SourceBufferIterator(SourceBuffer* aOwner, size_t aReadLimit) + : mOwner(aOwner), + mState(START), + mChunkCount(0), + mByteCount(0), + mRemainderToRead(aReadLimit) { + MOZ_ASSERT(aOwner); + mData.mIterating.mChunk = 0; + mData.mIterating.mData = nullptr; + mData.mIterating.mOffset = 0; + mData.mIterating.mAvailableLength = 0; + mData.mIterating.mNextReadLength = 0; + } + + SourceBufferIterator(SourceBufferIterator&& aOther) + : mOwner(std::move(aOther.mOwner)), + mState(aOther.mState), + mData(aOther.mData), + mChunkCount(aOther.mChunkCount), + mByteCount(aOther.mByteCount), + mRemainderToRead(aOther.mRemainderToRead) {} + + ~SourceBufferIterator(); + + SourceBufferIterator& operator=(SourceBufferIterator&& aOther); + + /** + * Returns true if there are no more than @aBytes remaining in the + * SourceBuffer. If the SourceBuffer is not yet complete, returns false. + */ + bool RemainingBytesIsNoMoreThan(size_t aBytes) const; + + /** + * Advances the iterator through the SourceBuffer if possible. Advances no + * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as + * possible.) + * + * This is a wrapper around AdvanceOrScheduleResume() that makes it clearer at + * the callsite when the no resuming is intended. + * + * @return State::READY if the iterator was successfully advanced. + * State::WAITING if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer, but the SourceBuffer + * may still receive additional data. + * State::COMPLETE if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer and the SourceBuffer is + * marked complete (i.e., it will never receive any additional + * data). + */ + State Advance(size_t aRequestedBytes) { + return AdvanceOrScheduleResume(aRequestedBytes, nullptr); + } + + /** + * Advances the iterator through the SourceBuffer if possible. Advances no + * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as + * possible.) If advancing is not possible and @aConsumer is not null, + * arranges to call the @aConsumer's Resume() method when more data is + * available. + * + * @return State::READY if the iterator was successfully advanced. + * State::WAITING if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer, but the SourceBuffer + * may still receive additional data. @aConsumer's Resume() method + * will be called when additional data is available. + * State::COMPLETE if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer and the SourceBuffer is + * marked complete (i.e., it will never receive any additional + * data). + */ + State AdvanceOrScheduleResume(size_t aRequestedBytes, IResumable* aConsumer); + + /// If at the end, returns the status passed to SourceBuffer::Complete(). + nsresult CompletionStatus() const { + MOZ_ASSERT(mState == COMPLETE, + "Calling CompletionStatus() in the wrong state"); + return mState == COMPLETE ? mData.mAtEnd.mStatus : NS_OK; + } + + /// If we're ready to read, returns a pointer to the new data. + const char* Data() const { + MOZ_ASSERT(mState == READY, "Calling Data() in the wrong state"); + return mState == READY ? mData.mIterating.mData + mData.mIterating.mOffset + : nullptr; + } + + /// If we're ready to read, returns the length of the new data. + size_t Length() const { + MOZ_ASSERT(mState == READY, "Calling Length() in the wrong state"); + return mState == READY ? mData.mIterating.mNextReadLength : 0; + } + + /// If we're ready to read, returns whether or not everything available thus + /// far has been in the same contiguous buffer. + bool IsContiguous() const { + MOZ_ASSERT(mState == READY, "Calling IsContiguous() in the wrong state"); + return mState == READY ? mData.mIterating.mChunk == 0 : false; + } + + /// @return a count of the chunks we've advanced through. + uint32_t ChunkCount() const { return mChunkCount; } + + /// @return a count of the bytes in all chunks we've advanced through. + size_t ByteCount() const { return mByteCount; } + + /// @return the source buffer which owns the iterator. + SourceBuffer* Owner() const { + MOZ_ASSERT(mOwner); + return mOwner; + } + + /// @return the current offset from the beginning of the buffer. + size_t Position() const { + return mByteCount - mData.mIterating.mAvailableLength; + } + + private: + friend class SourceBuffer; + + SourceBufferIterator(const SourceBufferIterator&) = delete; + SourceBufferIterator& operator=(const SourceBufferIterator&) = delete; + + bool HasMore() const { return mState != COMPLETE; } + + State AdvanceFromLocalBuffer(size_t aRequestedBytes) { + MOZ_ASSERT(mState == READY, "Advancing in the wrong state"); + MOZ_ASSERT(mData.mIterating.mAvailableLength > 0, + "The local buffer shouldn't be empty"); + MOZ_ASSERT(mData.mIterating.mNextReadLength == 0, + "Advancing without consuming previous data"); + + mData.mIterating.mNextReadLength = + std::min(mData.mIterating.mAvailableLength, aRequestedBytes); + + return READY; + } + + State SetReady(uint32_t aChunk, const char* aData, size_t aOffset, + size_t aAvailableLength, size_t aRequestedBytes) { + MOZ_ASSERT(mState != COMPLETE); + mState = READY; + + // Prevent the iterator from reporting more data than it is allowed to read. + if (aAvailableLength > mRemainderToRead) { + aAvailableLength = mRemainderToRead; + } + + // Update state. + mData.mIterating.mChunk = aChunk; + mData.mIterating.mData = aData; + mData.mIterating.mOffset = aOffset; + mData.mIterating.mAvailableLength = aAvailableLength; + + // Update metrics. + mChunkCount++; + mByteCount += aAvailableLength; + + // Attempt to advance by the requested number of bytes. + return AdvanceFromLocalBuffer(aRequestedBytes); + } + + State SetWaiting(bool aHasConsumer) { + MOZ_ASSERT(mState != COMPLETE); + // Without a consumer, we won't know when to wake up precisely. Caller + // convention should mean that we don't try to advance unless we have + // written new data, but that doesn't mean we got enough. + MOZ_ASSERT(mState != WAITING || !aHasConsumer, + "Did we get a spurious wakeup somehow?"); + return mState = WAITING; + } + + State SetComplete(nsresult aStatus) { + mData.mAtEnd.mStatus = aStatus; + return mState = COMPLETE; + } + + RefPtr mOwner; + + State mState; + + /** + * This union contains our iteration state if we're still iterating (for + * states START, READY, and WAITING) and the status the SourceBuffer was + * completed with if we're in state COMPLETE. + */ + union { + struct { + uint32_t mChunk; // Index of the chunk in SourceBuffer. + const char* mData; // Pointer to the start of the chunk. + size_t mOffset; // Current read position of the iterator relative to + // mData. + size_t mAvailableLength; // How many bytes remain unread in the chunk, + // relative to mOffset. + size_t + mNextReadLength; // How many bytes the last iterator advance + // requested to be read, so that we know much + // to increase mOffset and reduce mAvailableLength + // by when the next advance is requested. + } mIterating; // Cached info of the chunk currently iterating over. + struct { + nsresult mStatus; // Status code indicating if we read all the data. + } mAtEnd; // State info after iterator is complete. + } mData; + + uint32_t mChunkCount; // Count of chunks observed, including current chunk. + size_t mByteCount; // Count of readable bytes observed, including unread + // bytes from the current chunk. + size_t mRemainderToRead; // Count of bytes left to read if there is a maximum + // imposed by the caller. SIZE_MAX if unlimited. +}; + +/** + * SourceBuffer is a parallel data structure used for storing image source + * (compressed) data. + * + * SourceBuffer is a single producer, multiple consumer data structure. The + * single producer calls Append() to append data to the buffer. In parallel, + * multiple consumers can call Iterator(), which returns a SourceBufferIterator + * that they can use to iterate through the buffer. The SourceBufferIterator + * returns a series of pointers which remain stable for lifetime of the + * SourceBuffer, and the data they point to is immutable, ensuring that the + * producer never interferes with the consumers. + * + * In order to avoid blocking, SourceBuffer works with SourceBufferIterator to + * keep a list of consumers which are waiting for new data, and to resume them + * when the producer appends more. All consumers must implement the IResumable + * interface to make this possible. + */ +class SourceBuffer final { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(image::SourceBuffer) + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(image::SourceBuffer) + + SourceBuffer(); + + ////////////////////////////////////////////////////////////////////////////// + // Producer methods. + ////////////////////////////////////////////////////////////////////////////// + + /** + * If the producer knows how long the source data will be, it should call + * ExpectLength, which enables SourceBuffer to preallocate its buffer. + */ + nsresult ExpectLength(size_t aExpectedLength); + + /// Append the provided data to the buffer. + nsresult Append(const char* aData, size_t aLength); + + /// Append the data available on the provided nsIInputStream to the buffer. + nsresult AppendFromInputStream(nsIInputStream* aInputStream, uint32_t aCount); + + /** + * Mark the buffer complete, with a status that will be available to + * consumers. Further calls to Append() are forbidden after Complete(). + */ + void Complete(nsresult aStatus); + + /// Returns true if the buffer is complete. + bool IsComplete(); + + /// Memory reporting. + size_t SizeOfIncludingThisWithComputedFallback(MallocSizeOf) const; + + ////////////////////////////////////////////////////////////////////////////// + // Consumer methods. + ////////////////////////////////////////////////////////////////////////////// + + /** + * Returns an iterator to this SourceBuffer, which cannot read more than the + * given length. + */ + SourceBufferIterator Iterator(size_t aReadLength = SIZE_MAX); + + ////////////////////////////////////////////////////////////////////////////// + // Consumer methods. + ////////////////////////////////////////////////////////////////////////////// + + /** + * The minimum chunk capacity we'll allocate, if we don't know the correct + * capacity (which would happen because ExpectLength() wasn't called or gave + * us the wrong value). This is only exposed for use by tests; if normal code + * is using this, it's doing something wrong. + */ + static const size_t MIN_CHUNK_CAPACITY = 4096; + + /** + * The maximum chunk capacity we'll allocate. This was historically the + * maximum we would preallocate based on the network size. We may adjust it + * in the future based on the IMAGE_DECODE_CHUNKS telemetry to ensure most + * images remain in a single chunk. + */ + static const size_t MAX_CHUNK_CAPACITY = 20 * 1024 * 1024; + + private: + friend class SourceBufferIterator; + + ~SourceBuffer(); + + ////////////////////////////////////////////////////////////////////////////// + // Chunk type and chunk-related methods. + ////////////////////////////////////////////////////////////////////////////// + + class Chunk final { + public: + explicit Chunk(size_t aCapacity) : mCapacity(aCapacity), mLength(0) { + MOZ_ASSERT(aCapacity > 0, "Creating zero-capacity chunk"); + mData = static_cast(malloc(mCapacity)); + } + + ~Chunk() { free(mData); } + + Chunk(Chunk&& aOther) + : mCapacity(aOther.mCapacity), + mLength(aOther.mLength), + mData(aOther.mData) { + aOther.mCapacity = aOther.mLength = 0; + aOther.mData = nullptr; + } + + Chunk& operator=(Chunk&& aOther) { + free(mData); + mCapacity = aOther.mCapacity; + mLength = aOther.mLength; + mData = aOther.mData; + aOther.mCapacity = aOther.mLength = 0; + aOther.mData = nullptr; + return *this; + } + + bool AllocationFailed() const { return !mData; } + size_t Capacity() const { return mCapacity; } + size_t Length() const { return mLength; } + + char* Data() const { + MOZ_ASSERT(mData, "Allocation failed but nobody checked for it"); + return mData; + } + + void AddLength(size_t aAdditionalLength) { + MOZ_ASSERT(mLength + aAdditionalLength <= mCapacity); + mLength += aAdditionalLength; + } + + bool SetCapacity(size_t aCapacity) { + MOZ_ASSERT(mData, "Allocation failed but nobody checked for it"); + char* data = static_cast(realloc(mData, aCapacity)); + if (!data) { + return false; + } + + mData = data; + mCapacity = aCapacity; + return true; + } + + private: + Chunk(const Chunk&) = delete; + Chunk& operator=(const Chunk&) = delete; + + size_t mCapacity; + size_t mLength; + char* mData; + }; + + nsresult AppendChunk(Maybe&& aChunk) MOZ_REQUIRES(mMutex); + Maybe CreateChunk(size_t aCapacity, size_t aExistingCapacity = 0, + bool aRoundUp = true); + nsresult Compact() MOZ_REQUIRES(mMutex); + static size_t RoundedUpCapacity(size_t aCapacity); + size_t FibonacciCapacityWithMinimum(size_t aMinCapacity) MOZ_REQUIRES(mMutex); + + ////////////////////////////////////////////////////////////////////////////// + // Iterator / consumer methods. + ////////////////////////////////////////////////////////////////////////////// + + void AddWaitingConsumer(IResumable* aConsumer) MOZ_REQUIRES(mMutex); + void ResumeWaitingConsumers() MOZ_REQUIRES(mMutex); + + typedef SourceBufferIterator::State State; + + State AdvanceIteratorOrScheduleResume(SourceBufferIterator& aIterator, + size_t aRequestedBytes, + IResumable* aConsumer); + bool RemainingBytesIsNoMoreThan(const SourceBufferIterator& aIterator, + size_t aBytes) const; + + void OnIteratorRelease(); + + ////////////////////////////////////////////////////////////////////////////// + // Helper methods. + ////////////////////////////////////////////////////////////////////////////// + + nsresult HandleError(nsresult aError) MOZ_REQUIRES(mMutex); + bool IsEmpty() MOZ_REQUIRES(mMutex); + bool IsLastChunk(uint32_t aChunk) MOZ_REQUIRES(mMutex); + + ////////////////////////////////////////////////////////////////////////////// + // Member variables. + ////////////////////////////////////////////////////////////////////////////// + + /// All private members are protected by mMutex. + mutable Mutex mMutex; + + /// The data in this SourceBuffer, stored as a series of Chunks. + AutoTArray mChunks MOZ_GUARDED_BY(mMutex); + + /// Consumers which are waiting to be notified when new data is available. + nsTArray> mWaitingConsumers MOZ_GUARDED_BY(mMutex); + + /// If present, marks this SourceBuffer complete with the given final status. + Maybe mStatus MOZ_GUARDED_BY(mMutex); + + /// Count of active consumers. + uint32_t mConsumerCount MOZ_GUARDED_BY(mMutex); + + /// True if compacting has been performed. + bool mCompacted MOZ_GUARDED_BY(mMutex); +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_sourcebuffer_h diff --git a/image/StreamingLexer.h b/image/StreamingLexer.h new file mode 100644 index 0000000000..06dbabde1c --- /dev/null +++ b/image/StreamingLexer.h @@ -0,0 +1,751 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * StreamingLexer is a lexing framework designed to make it simple to write + * image decoders without worrying about the details of how the data is arriving + * from the network. + */ + +#ifndef mozilla_image_StreamingLexer_h +#define mozilla_image_StreamingLexer_h + +#include +#include +#include + +#include "SourceBuffer.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" + +namespace mozilla { +namespace image { + +/// Buffering behaviors for StreamingLexer transitions. +enum class BufferingStrategy { + BUFFERED, // Data will be buffered and processed in one chunk. + UNBUFFERED // Data will be processed as it arrives, in multiple chunks. +}; + +/// Control flow behaviors for StreamingLexer transitions. +enum class ControlFlowStrategy { + CONTINUE, // If there's enough data, proceed to the next state immediately. + YIELD // Yield to the caller before proceeding to the next state. +}; + +/// Possible terminal states for the lexer. +enum class TerminalState { SUCCESS, FAILURE }; + +/// Possible yield reasons for the lexer. +enum class Yield { + NEED_MORE_DATA, // The lexer cannot continue without more data. + OUTPUT_AVAILABLE // There is output available for the caller to consume. +}; + +/// The result of a call to StreamingLexer::Lex(). +typedef Variant LexerResult; + +/** + * LexerTransition is a type used to give commands to the lexing framework. + * Code that uses StreamingLexer can create LexerTransition values using the + * static methods on Transition, and then return them to the lexing framework + * for execution. + */ +template +class LexerTransition { + public: + // This is implicit so that Terminate{Success,Failure}() can return a + // TerminalState and have it implicitly converted to a + // LexerTransition, which avoids the need for a "" + // qualification to the Terminate{Success,Failure}() callsite. + MOZ_IMPLICIT LexerTransition(TerminalState aFinalState) + : mNextState(aFinalState) {} + + bool NextStateIsTerminal() const { + return mNextState.template is(); + } + + TerminalState NextStateAsTerminal() const { + return mNextState.template as(); + } + + State NextState() const { + return mNextState.template as().mState; + } + + State UnbufferedState() const { + return *mNextState.template as().mUnbufferedState; + } + + size_t Size() const { + return mNextState.template as().mSize; + } + + BufferingStrategy Buffering() const { + return mNextState.template as().mBufferingStrategy; + } + + ControlFlowStrategy ControlFlow() const { + return mNextState.template as().mControlFlowStrategy; + } + + private: + friend struct Transition; + + LexerTransition(State aNextState, const Maybe& aUnbufferedState, + size_t aSize, BufferingStrategy aBufferingStrategy, + ControlFlowStrategy aControlFlowStrategy) + : mNextState(NonTerminalState(aNextState, aUnbufferedState, aSize, + aBufferingStrategy, aControlFlowStrategy)) { + } + + struct NonTerminalState { + State mState; + Maybe mUnbufferedState; + size_t mSize; + BufferingStrategy mBufferingStrategy; + ControlFlowStrategy mControlFlowStrategy; + + NonTerminalState(State aState, const Maybe& aUnbufferedState, + size_t aSize, BufferingStrategy aBufferingStrategy, + ControlFlowStrategy aControlFlowStrategy) + : mState(aState), + mUnbufferedState(aUnbufferedState), + mSize(aSize), + mBufferingStrategy(aBufferingStrategy), + mControlFlowStrategy(aControlFlowStrategy) { + MOZ_ASSERT_IF(mBufferingStrategy == BufferingStrategy::UNBUFFERED, + mUnbufferedState); + MOZ_ASSERT_IF(mUnbufferedState, + mBufferingStrategy == BufferingStrategy::UNBUFFERED); + } + }; + + Variant mNextState; +}; + +struct Transition { + /// Transition to @aNextState, buffering @aSize bytes of data. + template + static LexerTransition To(const State& aNextState, size_t aSize) { + return LexerTransition(aNextState, Nothing(), aSize, + BufferingStrategy::BUFFERED, + ControlFlowStrategy::CONTINUE); + } + + /// Yield to the caller, transitioning to @aNextState when Lex() is next + /// invoked. The same data that was delivered for the current state will be + /// delivered again. + template + static LexerTransition ToAfterYield(const State& aNextState) { + return LexerTransition(aNextState, Nothing(), 0, + BufferingStrategy::BUFFERED, + ControlFlowStrategy::YIELD); + } + + /** + * Transition to @aNextState via @aUnbufferedState, reading @aSize bytes of + * data unbuffered. + * + * The unbuffered data will be delivered in state @aUnbufferedState, which may + * be invoked repeatedly until all @aSize bytes have been delivered. Then, + * @aNextState will be invoked with no data. No state transitions are allowed + * from @aUnbufferedState except for transitions to a terminal state, so + * @aNextState will always be reached unless lexing terminates early. + */ + template + static LexerTransition ToUnbuffered(const State& aNextState, + const State& aUnbufferedState, + size_t aSize) { + return LexerTransition(aNextState, Some(aUnbufferedState), aSize, + BufferingStrategy::UNBUFFERED, + ControlFlowStrategy::CONTINUE); + } + + /** + * Continue receiving unbuffered data. @aUnbufferedState should be the same + * state as the @aUnbufferedState specified in the preceding call to + * ToUnbuffered(). + * + * This should be used during an unbuffered read initiated by ToUnbuffered(). + */ + template + static LexerTransition ContinueUnbuffered( + const State& aUnbufferedState) { + return LexerTransition(aUnbufferedState, Nothing(), 0, + BufferingStrategy::BUFFERED, + ControlFlowStrategy::CONTINUE); + } + + /** + * Continue receiving unbuffered data. @aUnbufferedState should be the same + * state as the @aUnbufferedState specified in the preceding call to + * ToUnbuffered(). @aSize indicates the amount of data that has already been + * consumed; the next state will receive the same data that was delivered to + * the current state, without the first @aSize bytes. + * + * This should be used during an unbuffered read initiated by ToUnbuffered(). + */ + template + static LexerTransition ContinueUnbufferedAfterYield( + const State& aUnbufferedState, size_t aSize) { + return LexerTransition(aUnbufferedState, Nothing(), aSize, + BufferingStrategy::BUFFERED, + ControlFlowStrategy::YIELD); + } + + /** + * Terminate lexing, ending up in terminal state SUCCESS. (The implicit + * LexerTransition constructor will convert the result to a LexerTransition + * as needed.) + * + * No more data will be delivered after this function is used. + */ + static TerminalState TerminateSuccess() { return TerminalState::SUCCESS; } + + /** + * Terminate lexing, ending up in terminal state FAILURE. (The implicit + * LexerTransition constructor will convert the result to a LexerTransition + * as needed.) + * + * No more data will be delivered after this function is used. + */ + static TerminalState TerminateFailure() { return TerminalState::FAILURE; } + + private: + Transition(); +}; + +/** + * StreamingLexer is a lexing framework designed to make it simple to write + * image decoders without worrying about the details of how the data is arriving + * from the network. + * + * To use StreamingLexer: + * + * - Create a State type. This should be an |enum class| listing all of the + * states that you can be in while lexing the image format you're trying to + * read. + * + * - Add an instance of StreamingLexer to your decoder class. Initialize + * it with a Transition::To() the state that you want to start lexing in, and + * a Transition::To() the state you'd like to use to handle truncated data. + * + * - In your decoder's DoDecode() method, call Lex(), passing in the input + * data and length that are passed to DoDecode(). You also need to pass + * a lambda which dispatches to lexing code for each state based on the State + * value that's passed in. The lambda generally should just continue a + * |switch| statement that calls different methods for each State value. Each + * method should return a LexerTransition, which the lambda should + * return in turn. + * + * - Write the methods that actually implement lexing for your image format. + * These methods should return either Transition::To(), to move on to another + * state, or Transition::Terminate{Success,Failure}(), if lexing has + * terminated in either success or failure. (There are also additional + * transitions for unbuffered reads; see below.) + * + * That's the basics. The StreamingLexer will track your position in the input + * and buffer enough data so that your lexing methods can process everything in + * one pass. Lex() returns Yield::NEED_MORE_DATA if more data is needed, in + * which case you should just return from DoDecode(). If lexing reaches a + * terminal state, Lex() returns TerminalState::SUCCESS or + * TerminalState::FAILURE, and you can check which one to determine if lexing + * succeeded or failed and do any necessary cleanup. + * + * Sometimes, the input data is truncated. StreamingLexer will notify you when + * this happens by invoking the truncated data state you passed to the + * constructor. At this point you can attempt to recover and return + * TerminalState::SUCCESS or TerminalState::FAILURE, depending on whether you + * were successful. Note that you can't return anything other than a terminal + * state in this situation, since there's no more data to read. For the same + * reason, your truncated data state shouldn't require any data. (That is, the + * @aSize argument you pass to Transition::To() must be zero.) Violating these + * requirements will trigger assertions and an immediate transition to + * TerminalState::FAILURE. + * + * Some lexers may want to *avoid* buffering in some cases, and just process the + * data as it comes in. This is useful if, for example, you just want to skip + * over a large section of data; there's no point in buffering data you're just + * going to ignore. + * + * You can begin an unbuffered read with Transition::ToUnbuffered(). This works + * a little differently than Transition::To() in that you specify *two* states. + * The @aUnbufferedState argument specifies a state that will be called + * repeatedly with unbuffered data, as soon as it arrives. The implementation + * for that state should return either a transition to a terminal state, or a + * Transition::ContinueUnbuffered() to the same @aUnbufferedState. (From a + * technical perspective, it's not necessary to specify the state again, but + * it's helpful to human readers.) Once the amount of data requested in the + * original call to Transition::ToUnbuffered() has been delivered, Lex() will + * transition to the @aNextState state specified via Transition::ToUnbuffered(). + * That state will be invoked with *no* data; it's just called to signal that + * the unbuffered read is over. + * + * It's sometimes useful for a lexer to provide incremental results, rather + * than simply running to completion and presenting all its output at once. For + * example, when decoding animated images, it may be useful to produce each + * frame incrementally. StreamingLexer supports this by allowing a lexer to + * yield. + * + * To yield back to the caller, a state implementation can simply return + * Transition::ToAfterYield(). ToAfterYield()'s @aNextState argument specifies + * the next state that the lexer should transition to, just like when using + * Transition::To(), but there are two differences. One is that Lex() will + * return to the caller before processing any more data when it encounters a + * yield transition. This provides an opportunity for the caller to interact + * with the lexer's intermediate results. The second difference is that + * @aNextState will be called with *the same data as the state that you returned + * Transition::ToAfterYield() from*. This allows a lexer to partially consume + * the data, return intermediate results, and then finish consuming the data + * when @aNextState is called. + * + * It's also possible to yield during an unbuffered read. Just return a + * Transition::ContinueUnbufferedAfterYield(). Just like with + * Transition::ContinueUnbuffered(), the @aUnbufferedState must be the same as + * the one originally passed to Transition::ToUnbuffered(). The second argument, + * @aSize, specifies the amount of data that the lexer has already consumed. + * When @aUnbufferedState is next invoked, it will get the same data that it + * received previously, except that the first @aSize bytes will be excluded. + * This makes it easy to consume unbuffered data incrementally. + * + * XXX(seth): We should be able to get of the |State| stuff totally once bug + * 1198451 lands, since we can then just return a function representing the next + * state directly. + */ +template +class StreamingLexer { + public: + StreamingLexer(const LexerTransition& aStartState, + const LexerTransition& aTruncatedState) + : mTransition(TerminalState::FAILURE), + mTruncatedTransition(aTruncatedState) { + if (!aStartState.NextStateIsTerminal() && + aStartState.ControlFlow() == ControlFlowStrategy::YIELD) { + // Allowing a StreamingLexer to start in a yield state doesn't make sense + // semantically (since yield states are supposed to deliver the same data + // as previous states, and there's no previous state here), but more + // importantly, it's necessary to advance a SourceBufferIterator at least + // once before you can read from it, and adding the necessary checks to + // Lex() to avoid that issue has the potential to mask real bugs. So + // instead, it's better to forbid starting in a yield state. + MOZ_ASSERT_UNREACHABLE("Starting in a yield state"); + return; + } + + if (!aTruncatedState.NextStateIsTerminal() && + (aTruncatedState.ControlFlow() == ControlFlowStrategy::YIELD || + aTruncatedState.Buffering() == BufferingStrategy::UNBUFFERED || + aTruncatedState.Size() != 0)) { + // The truncated state can't receive any data because, by definition, + // there is no more data to receive. That means that yielding or an + // unbuffered read would not make sense, and that the state must require + // zero bytes. + MOZ_ASSERT_UNREACHABLE("Truncated state makes no sense"); + return; + } + + SetTransition(aStartState); + } + + /** + * From the given SourceBufferIterator, aIterator, create a new iterator at + * the same position, with the given read limit, aReadLimit. The read limit + * applies after adjusting for the position. If the given iterator has been + * advanced, but required buffering inside StreamingLexer, the position + * of the cloned iterator will be at the beginning of buffered data; this + * should match the perspective of the caller. + */ + Maybe Clone(SourceBufferIterator& aIterator, + size_t aReadLimit) const { + // In order to advance to the current position of the iterator from the + // perspective of the caller, we need to take into account if we are + // buffering data. + size_t pos = aIterator.Position(); + if (!mBuffer.empty()) { + pos += aIterator.Length(); + MOZ_ASSERT(pos > mBuffer.length()); + pos -= mBuffer.length(); + } + + size_t readLimit = aReadLimit; + if (aReadLimit != SIZE_MAX) { + readLimit += pos; + } + + SourceBufferIterator other = aIterator.Owner()->Iterator(readLimit); + + // Since the current iterator has already advanced to this point, we + // know that the state can only be READY or COMPLETE. That does not mean + // everything is stored in a single chunk, and may require multiple Advance + // calls to get where we want to be. + SourceBufferIterator::State state; + do { + state = other.Advance(pos); + if (state != SourceBufferIterator::READY) { + // The only way we should fail to advance over data we already seen is + // if we hit an error while inserting data into the buffer. This will + // cause an early exit. + MOZ_ASSERT(NS_FAILED(other.CompletionStatus())); + return Nothing(); + } + MOZ_ASSERT(pos >= other.Length()); + pos -= other.Length(); + } while (pos > 0); + + // Force the data pointer to be where we expect it to be. + state = other.Advance(0); + if (state != SourceBufferIterator::READY) { + // The current position could be the end of the buffer, in which case + // there is no point cloning with no more data to read. + MOZ_ASSERT(state == SourceBufferIterator::COMPLETE); + return Nothing(); + } + return Some(std::move(other)); + } + + template + LexerResult Lex(SourceBufferIterator& aIterator, IResumable* aOnResume, + Func aFunc) { + if (mTransition.NextStateIsTerminal()) { + // We've already reached a terminal state. We never deliver any more data + // in this case; just return the terminal state again immediately. + return LexerResult(mTransition.NextStateAsTerminal()); + } + + Maybe result; + + // If the lexer requested a yield last time, we deliver the same data again + // before we read anything else from |aIterator|. Note that although to the + // callers of Lex(), Yield::NEED_MORE_DATA is just another type of yield, + // internally they're different in that we don't redeliver the same data in + // the Yield::NEED_MORE_DATA case, and |mYieldingToState| is not set. This + // means that for Yield::NEED_MORE_DATA, we go directly to the loop below. + if (mYieldingToState) { + result = mTransition.Buffering() == BufferingStrategy::UNBUFFERED + ? UnbufferedReadAfterYield(aIterator, aFunc) + : BufferedReadAfterYield(aIterator, aFunc); + } + + while (!result) { + MOZ_ASSERT_IF(mTransition.Buffering() == BufferingStrategy::UNBUFFERED, + mUnbufferedState); + + // Figure out how much we need to read. + const size_t toRead = + mTransition.Buffering() == BufferingStrategy::UNBUFFERED + ? mUnbufferedState->mBytesRemaining + : mTransition.Size() - mBuffer.length(); + + // Attempt to advance the iterator by |toRead| bytes. + switch (aIterator.AdvanceOrScheduleResume(toRead, aOnResume)) { + case SourceBufferIterator::WAITING: + // We can't continue because the rest of the data hasn't arrived from + // the network yet. We don't have to do anything special; the + // SourceBufferIterator will ensure that |aOnResume| gets called when + // more data is available. + result = Some(LexerResult(Yield::NEED_MORE_DATA)); + break; + + case SourceBufferIterator::COMPLETE: + // The data is truncated; if not, the lexer would've reached a + // terminal state by now. We only get to + // SourceBufferIterator::COMPLETE after every byte of data has been + // delivered to the lexer. + result = Truncated(aIterator, aFunc); + break; + + case SourceBufferIterator::READY: + // Process the new data that became available. + MOZ_ASSERT(aIterator.Data()); + + result = mTransition.Buffering() == BufferingStrategy::UNBUFFERED + ? UnbufferedRead(aIterator, aFunc) + : BufferedRead(aIterator, aFunc); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown SourceBufferIterator state"); + result = SetTransition(Transition::TerminateFailure()); + } + }; + + return *result; + } + + private: + template + Maybe UnbufferedRead(SourceBufferIterator& aIterator, + Func aFunc) { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED); + MOZ_ASSERT(mUnbufferedState); + MOZ_ASSERT(!mYieldingToState); + MOZ_ASSERT(mBuffer.empty(), + "Buffered read at the same time as unbuffered read?"); + MOZ_ASSERT(aIterator.Length() <= mUnbufferedState->mBytesRemaining, + "Read too much data during unbuffered read?"); + MOZ_ASSERT(mUnbufferedState->mBytesConsumedInCurrentChunk == 0, + "Already consumed data in the current chunk, but not yielding?"); + + if (mUnbufferedState->mBytesRemaining == 0) { + // We're done with the unbuffered read, so transition to the next state. + return SetTransition(aFunc(mTransition.NextState(), nullptr, 0)); + } + + return ContinueUnbufferedRead(aIterator.Data(), aIterator.Length(), + aIterator.Length(), aFunc); + } + + template + Maybe UnbufferedReadAfterYield(SourceBufferIterator& aIterator, + Func aFunc) { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED); + MOZ_ASSERT(mUnbufferedState); + MOZ_ASSERT(mYieldingToState); + MOZ_ASSERT(mBuffer.empty(), + "Buffered read at the same time as unbuffered read?"); + MOZ_ASSERT(aIterator.Length() <= mUnbufferedState->mBytesRemaining, + "Read too much data during unbuffered read?"); + MOZ_ASSERT( + mUnbufferedState->mBytesConsumedInCurrentChunk <= aIterator.Length(), + "Consumed more data than the current chunk holds?"); + MOZ_ASSERT(mTransition.UnbufferedState() == *mYieldingToState); + + mYieldingToState = Nothing(); + + if (mUnbufferedState->mBytesRemaining == 0) { + // We're done with the unbuffered read, so transition to the next state. + return SetTransition(aFunc(mTransition.NextState(), nullptr, 0)); + } + + // Since we've yielded, we may have already consumed some data in this + // chunk. Make the necessary adjustments. (Note that the std::min call is + // just belt-and-suspenders to keep this code memory safe even if there's + // a bug somewhere.) + const size_t toSkip = std::min( + mUnbufferedState->mBytesConsumedInCurrentChunk, aIterator.Length()); + const char* data = aIterator.Data() + toSkip; + const size_t length = aIterator.Length() - toSkip; + + // If |length| is zero, we've hit the end of the current chunk. This only + // happens if we yield right at the end of a chunk. Rather than call |aFunc| + // with a |length| of zero bytes (which seems potentially surprising to + // decoder authors), we go ahead and read more data. + if (length == 0) { + return FinishCurrentChunkOfUnbufferedRead(aIterator.Length()); + } + + return ContinueUnbufferedRead(data, length, aIterator.Length(), aFunc); + } + + template + Maybe ContinueUnbufferedRead(const char* aData, size_t aLength, + size_t aChunkLength, Func aFunc) { + // Call aFunc with the unbuffered state to indicate that we're in the + // middle of an unbuffered read. We enforce that any state transition + // passed back to us is either a terminal state or takes us back to the + // unbuffered state. + LexerTransition unbufferedTransition = + aFunc(mTransition.UnbufferedState(), aData, aLength); + + // If we reached a terminal state, we're done. + if (unbufferedTransition.NextStateIsTerminal()) { + return SetTransition(unbufferedTransition); + } + + MOZ_ASSERT(mTransition.UnbufferedState() == + unbufferedTransition.NextState()); + + // Perform bookkeeping. + if (unbufferedTransition.ControlFlow() == ControlFlowStrategy::YIELD) { + mUnbufferedState->mBytesConsumedInCurrentChunk += + unbufferedTransition.Size(); + return SetTransition(unbufferedTransition); + } + + MOZ_ASSERT(unbufferedTransition.Size() == 0); + return FinishCurrentChunkOfUnbufferedRead(aChunkLength); + } + + Maybe FinishCurrentChunkOfUnbufferedRead(size_t aChunkLength) { + // We've finished an unbuffered read of a chunk of length |aChunkLength|, so + // update |myBytesRemaining| to reflect that we're |aChunkLength| closer to + // the end of the unbuffered read. (The std::min call is just + // belt-and-suspenders to keep this code memory safe even if there's a bug + // somewhere.) + mUnbufferedState->mBytesRemaining -= + std::min(mUnbufferedState->mBytesRemaining, aChunkLength); + + // Since we're moving on to a new chunk, we can forget about the count of + // bytes consumed by yielding in the current chunk. + mUnbufferedState->mBytesConsumedInCurrentChunk = 0; + + return Nothing(); // Keep processing. + } + + template + Maybe BufferedRead(SourceBufferIterator& aIterator, Func aFunc) { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED); + MOZ_ASSERT(!mYieldingToState); + MOZ_ASSERT(!mUnbufferedState, + "Buffered read at the same time as unbuffered read?"); + MOZ_ASSERT(mBuffer.length() < mTransition.Size() || + (mBuffer.length() == 0 && mTransition.Size() == 0), + "Buffered more than we needed?"); + + // If we have all the data, we don't actually need to buffer anything. + if (mBuffer.empty() && aIterator.Length() == mTransition.Size()) { + return SetTransition( + aFunc(mTransition.NextState(), aIterator.Data(), aIterator.Length())); + } + + // We do need to buffer, so make sure the buffer has enough capacity. We + // deliberately wait until we know for sure we need to buffer to call + // reserve() since it could require memory allocation. + if (!mBuffer.reserve(mTransition.Size())) { + return SetTransition(Transition::TerminateFailure()); + } + + // Append the new data we just got to the buffer. + if (!mBuffer.append(aIterator.Data(), aIterator.Length())) { + return SetTransition(Transition::TerminateFailure()); + } + + if (mBuffer.length() != mTransition.Size()) { + return Nothing(); // Keep processing. + } + + // We've buffered everything, so transition to the next state. + return SetTransition( + aFunc(mTransition.NextState(), mBuffer.begin(), mBuffer.length())); + } + + template + Maybe BufferedReadAfterYield(SourceBufferIterator& aIterator, + Func aFunc) { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED); + MOZ_ASSERT(mYieldingToState); + MOZ_ASSERT(!mUnbufferedState, + "Buffered read at the same time as unbuffered read?"); + MOZ_ASSERT(mBuffer.length() <= mTransition.Size(), + "Buffered more than we needed?"); + + State nextState = std::move(*mYieldingToState); + + // After a yield, we need to take the same data that we delivered to the + // last state, and deliver it again to the new state. We know that this is + // happening right at a state transition, and that the last state was a + // buffered read, so there are two cases: + + // 1. We got the data from the SourceBufferIterator directly. + if (mBuffer.empty() && aIterator.Length() == mTransition.Size()) { + return SetTransition( + aFunc(nextState, aIterator.Data(), aIterator.Length())); + } + + // 2. We got the data from the buffer. + if (mBuffer.length() == mTransition.Size()) { + return SetTransition(aFunc(nextState, mBuffer.begin(), mBuffer.length())); + } + + // Anything else indicates a bug. + MOZ_ASSERT_UNREACHABLE("Unexpected state encountered during yield"); + return SetTransition(Transition::TerminateFailure()); + } + + template + Maybe Truncated(SourceBufferIterator& aIterator, Func aFunc) { + // The data is truncated. Let the lexer clean up and decide which terminal + // state we should end up in. + LexerTransition transition = + mTruncatedTransition.NextStateIsTerminal() + ? mTruncatedTransition + : aFunc(mTruncatedTransition.NextState(), nullptr, 0); + + if (!transition.NextStateIsTerminal()) { + MOZ_ASSERT_UNREACHABLE("Truncated state didn't lead to terminal state?"); + return SetTransition(Transition::TerminateFailure()); + } + + // If the SourceBuffer was completed with a failing state, we end in + // TerminalState::FAILURE no matter what. This only happens in exceptional + // situations like SourceBuffer itself encountering a failure due to OOM. + if (NS_FAILED(aIterator.CompletionStatus())) { + return SetTransition(Transition::TerminateFailure()); + } + + return SetTransition(transition); + } + + Maybe SetTransition(const LexerTransition& aTransition) { + // There should be no transitions while we're buffering for a buffered read + // unless they're to terminal states. (The terminal state transitions would + // generally be triggered by error handling code.) + MOZ_ASSERT_IF(!mBuffer.empty(), aTransition.NextStateIsTerminal() || + mBuffer.length() == mTransition.Size()); + + // Similarly, the only transitions allowed in the middle of an unbuffered + // read are to a terminal state, or a yield to the same state. Otherwise, we + // should remain in the same state until the unbuffered read completes. + MOZ_ASSERT_IF( + mUnbufferedState, + aTransition.NextStateIsTerminal() || + (aTransition.ControlFlow() == ControlFlowStrategy::YIELD && + aTransition.NextState() == mTransition.UnbufferedState()) || + mUnbufferedState->mBytesRemaining == 0); + + // If this transition is a yield, save the next state and return. We'll + // handle the rest when Lex() gets called again. + if (!aTransition.NextStateIsTerminal() && + aTransition.ControlFlow() == ControlFlowStrategy::YIELD) { + mYieldingToState = Some(aTransition.NextState()); + return Some(LexerResult(Yield::OUTPUT_AVAILABLE)); + } + + // Update our transition. + mTransition = aTransition; + + // Get rid of anything left over from the previous state. + mBuffer.clear(); + mYieldingToState = Nothing(); + mUnbufferedState = Nothing(); + + // If we reached a terminal state, let the caller know. + if (mTransition.NextStateIsTerminal()) { + return Some(LexerResult(mTransition.NextStateAsTerminal())); + } + + // If we're entering an unbuffered state, record how long we'll stay in it. + if (mTransition.Buffering() == BufferingStrategy::UNBUFFERED) { + mUnbufferedState.emplace(mTransition.Size()); + } + + return Nothing(); // Keep processing. + } + + // State that tracks our position within an unbuffered read. + struct UnbufferedState { + explicit UnbufferedState(size_t aBytesRemaining) + : mBytesRemaining(aBytesRemaining), mBytesConsumedInCurrentChunk(0) {} + + size_t mBytesRemaining; + size_t mBytesConsumedInCurrentChunk; + }; + + Vector mBuffer; + LexerTransition mTransition; + const LexerTransition mTruncatedTransition; + Maybe mYieldingToState; + Maybe mUnbufferedState; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_StreamingLexer_h diff --git a/image/SurfaceCache.cpp b/image/SurfaceCache.cpp new file mode 100644 index 0000000000..a514f41b7c --- /dev/null +++ b/image/SurfaceCache.cpp @@ -0,0 +1,1970 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * SurfaceCache is a service for caching temporary surfaces in imagelib. + */ + +#include "SurfaceCache.h" + +#include +#include + +#include "ISurfaceProvider.h" +#include "Image.h" +#include "LookupResult.h" +#include "ShutdownTracker.h" +#include "gfx2DGlue.h" +#include "gfxPlatform.h" +#include "imgFrame.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Likely.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/StaticPtr.h" + +#include "nsExpirationTracker.h" +#include "nsHashKeys.h" +#include "nsIMemoryReporter.h" +#include "nsRefPtrHashtable.h" +#include "nsSize.h" +#include "nsTArray.h" +#include "Orientation.h" +#include "prsystem.h" + +using std::max; +using std::min; + +namespace mozilla { + +using namespace gfx; + +namespace image { + +MOZ_DEFINE_MALLOC_SIZE_OF(SurfaceCacheMallocSizeOf) + +class CachedSurface; +class SurfaceCacheImpl; + +/////////////////////////////////////////////////////////////////////////////// +// Static Data +/////////////////////////////////////////////////////////////////////////////// + +// The single surface cache instance. +static StaticRefPtr sInstance; + +// The mutex protecting the surface cache. +static StaticMutex sInstanceMutex MOZ_UNANNOTATED; + +/////////////////////////////////////////////////////////////////////////////// +// SurfaceCache Implementation +/////////////////////////////////////////////////////////////////////////////// + +/** + * Cost models the cost of storing a surface in the cache. Right now, this is + * simply an estimate of the size of the surface in bytes, but in the future it + * may be worth taking into account the cost of rematerializing the surface as + * well. + */ +typedef size_t Cost; + +static Cost ComputeCost(const IntSize& aSize, uint32_t aBytesPerPixel) { + MOZ_ASSERT(aBytesPerPixel == 1 || aBytesPerPixel == 4); + return aSize.width * aSize.height * aBytesPerPixel; +} + +/** + * Since we want to be able to make eviction decisions based on cost, we need to + * be able to look up the CachedSurface which has a certain cost as well as the + * cost associated with a certain CachedSurface. To make this possible, in data + * structures we actually store a CostEntry, which contains a weak pointer to + * its associated surface. + * + * To make usage of the weak pointer safe, SurfaceCacheImpl always calls + * StartTracking after a surface is stored in the cache and StopTracking before + * it is removed. + */ +class CostEntry { + public: + CostEntry(NotNull aSurface, Cost aCost) + : mSurface(aSurface), mCost(aCost) {} + + NotNull Surface() const { return mSurface; } + Cost GetCost() const { return mCost; } + + bool operator==(const CostEntry& aOther) const { + return mSurface == aOther.mSurface && mCost == aOther.mCost; + } + + bool operator<(const CostEntry& aOther) const { + return mCost < aOther.mCost || + (mCost == aOther.mCost && mSurface < aOther.mSurface); + } + + private: + NotNull mSurface; + Cost mCost; +}; + +/** + * A CachedSurface associates a surface with a key that uniquely identifies that + * surface. + */ +class CachedSurface { + ~CachedSurface() {} + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(CachedSurface) + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CachedSurface) + + explicit CachedSurface(NotNull aProvider) + : mProvider(aProvider), mIsLocked(false) {} + + DrawableSurface GetDrawableSurface() const { + if (MOZ_UNLIKELY(IsPlaceholder())) { + MOZ_ASSERT_UNREACHABLE("Called GetDrawableSurface() on a placeholder"); + return DrawableSurface(); + } + + return mProvider->Surface(); + } + + DrawableSurface GetDrawableSurfaceEvenIfPlaceholder() const { + return mProvider->Surface(); + } + + void SetLocked(bool aLocked) { + if (IsPlaceholder()) { + return; // Can't lock a placeholder. + } + + // Update both our state and our provider's state. Some surface providers + // are permanently locked; maintaining our own locking state enables us to + // respect SetLocked() even when it's meaningless from the provider's + // perspective. + mIsLocked = aLocked; + mProvider->SetLocked(aLocked); + } + + bool IsLocked() const { + return !IsPlaceholder() && mIsLocked && mProvider->IsLocked(); + } + + void SetCannotSubstitute() { + mProvider->Availability().SetCannotSubstitute(); + } + bool CannotSubstitute() const { + return mProvider->Availability().CannotSubstitute(); + } + + bool IsPlaceholder() const { + return mProvider->Availability().IsPlaceholder(); + } + bool IsDecoded() const { return !IsPlaceholder() && mProvider->IsFinished(); } + + ImageKey GetImageKey() const { return mProvider->GetImageKey(); } + const SurfaceKey& GetSurfaceKey() const { return mProvider->GetSurfaceKey(); } + nsExpirationState* GetExpirationState() { return &mExpirationState; } + + CostEntry GetCostEntry() { + return image::CostEntry(WrapNotNull(this), mProvider->LogicalSizeInBytes()); + } + + size_t ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + aMallocSizeOf(mProvider.get()); + } + + void InvalidateRecording() { mProvider->InvalidateRecording(); } + + // A helper type used by SurfaceCacheImpl::CollectSizeOfSurfaces. + struct MOZ_STACK_CLASS SurfaceMemoryReport { + SurfaceMemoryReport(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) + : mCounters(aCounters), mMallocSizeOf(aMallocSizeOf) {} + + void Add(NotNull aCachedSurface, bool aIsFactor2) { + if (aCachedSurface->IsPlaceholder()) { + return; + } + + // Record the memory used by the ISurfaceProvider. This may not have a + // straightforward relationship to the size of the surface that + // DrawableRef() returns if the surface is generated dynamically. (i.e., + // for surfaces with PlaybackType::eAnimated.) + aCachedSurface->mProvider->AddSizeOfExcludingThis( + mMallocSizeOf, [&](ISurfaceProvider::AddSizeOfCbData& aMetadata) { + SurfaceMemoryCounter counter(aCachedSurface->GetSurfaceKey(), + aCachedSurface->IsLocked(), + aCachedSurface->CannotSubstitute(), + aIsFactor2, aMetadata.mFinished); + + counter.Values().SetDecodedHeap(aMetadata.mHeapBytes); + counter.Values().SetDecodedNonHeap(aMetadata.mNonHeapBytes); + counter.Values().SetDecodedUnknown(aMetadata.mUnknownBytes); + counter.Values().SetExternalHandles(aMetadata.mExternalHandles); + counter.Values().SetFrameIndex(aMetadata.mIndex); + counter.Values().SetExternalId(aMetadata.mExternalId); + counter.Values().SetSurfaceTypes(aMetadata.mTypes); + + mCounters.AppendElement(counter); + }); + } + + private: + nsTArray& mCounters; + MallocSizeOf mMallocSizeOf; + }; + + private: + nsExpirationState mExpirationState; + NotNull> mProvider; + bool mIsLocked; +}; + +static int64_t AreaOfIntSize(const IntSize& aSize) { + return static_cast(aSize.width) * static_cast(aSize.height); +} + +/** + * An ImageSurfaceCache is a per-image surface cache. For correctness we must be + * able to remove all surfaces associated with an image when the image is + * destroyed or invalidated. Since this will happen frequently, it makes sense + * to make it cheap by storing the surfaces for each image separately. + * + * ImageSurfaceCache also keeps track of whether its associated image is locked + * or unlocked. + * + * The cache may also enter "factor of 2" mode which occurs when the number of + * surfaces in the cache exceeds the "image.cache.factor2.threshold-surfaces" + * pref plus the number of native sizes of the image. When in "factor of 2" + * mode, the cache will strongly favour sizes which are a factor of 2 of the + * largest native size. It accomplishes this by suggesting a factor of 2 size + * when lookups fail and substituting the nearest factor of 2 surface to the + * ideal size as the "best" available (as opposed to substitution but not + * found). This allows us to minimize memory consumption and CPU time spent + * decoding when a website requires many variants of the same surface. + */ +class ImageSurfaceCache { + ~ImageSurfaceCache() {} + + public: + explicit ImageSurfaceCache(const ImageKey aImageKey) + : mLocked(false), + mFactor2Mode(false), + mFactor2Pruned(false), + mIsVectorImage(aImageKey->GetType() == imgIContainer::TYPE_VECTOR) {} + + MOZ_DECLARE_REFCOUNTED_TYPENAME(ImageSurfaceCache) + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageSurfaceCache) + + typedef nsRefPtrHashtable, CachedSurface> + SurfaceTable; + + auto Values() const { return mSurfaces.Values(); } + uint32_t Count() const { return mSurfaces.Count(); } + bool IsEmpty() const { return mSurfaces.Count() == 0; } + + size_t ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t bytes = aMallocSizeOf(this) + + mSurfaces.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& value : Values()) { + bytes += value->ShallowSizeOfIncludingThis(aMallocSizeOf); + } + return bytes; + } + + [[nodiscard]] bool Insert(NotNull aSurface) { + MOZ_ASSERT(!mLocked || aSurface->IsPlaceholder() || aSurface->IsLocked(), + "Inserting an unlocked surface for a locked image"); + const auto& surfaceKey = aSurface->GetSurfaceKey(); + if (surfaceKey.Region()) { + // We don't allow substitutes for surfaces with regions, so we don't want + // to allow factor of 2 mode pruning to release these surfaces. + aSurface->SetCannotSubstitute(); + } + return mSurfaces.InsertOrUpdate(surfaceKey, RefPtr{aSurface}, + fallible); + } + + already_AddRefed Remove(NotNull aSurface) { + MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()), + "Should not be removing a surface we don't have"); + + RefPtr surface; + mSurfaces.Remove(aSurface->GetSurfaceKey(), getter_AddRefs(surface)); + AfterMaybeRemove(); + return surface.forget(); + } + + already_AddRefed Lookup(const SurfaceKey& aSurfaceKey, + bool aForAccess) { + RefPtr surface; + mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface)); + + if (aForAccess) { + if (surface) { + // We don't want to allow factor of 2 mode pruning to release surfaces + // for which the callers will accept no substitute. + surface->SetCannotSubstitute(); + } else if (!mFactor2Mode) { + // If no exact match is found, and this is for use rather than internal + // accounting (i.e. insert and removal), we know this will trigger a + // decode. Make sure we switch now to factor of 2 mode if necessary. + MaybeSetFactor2Mode(); + } + } + + return surface.forget(); + } + + /** + * @returns A tuple containing the best matching CachedSurface if available, + * a MatchType describing how the CachedSurface was selected, and + * an IntSize which is the size the caller should choose to decode + * at should it attempt to do so. + */ + std::tuple, MatchType, IntSize> + LookupBestMatch(const SurfaceKey& aIdealKey) { + // Try for an exact match first. + RefPtr exactMatch; + mSurfaces.Get(aIdealKey, getter_AddRefs(exactMatch)); + if (exactMatch) { + if (exactMatch->IsDecoded()) { + return std::make_tuple(exactMatch.forget(), MatchType::EXACT, + IntSize()); + } + } else if (aIdealKey.Region()) { + // We cannot substitute if we have a region. Allow it to create an exact + // match. + return std::make_tuple(exactMatch.forget(), MatchType::NOT_FOUND, + IntSize()); + } else if (!mFactor2Mode) { + // If no exact match is found, and we are not in factor of 2 mode, then + // we know that we will trigger a decode because at best we will provide + // a substitute. Make sure we switch now to factor of 2 mode if necessary. + MaybeSetFactor2Mode(); + } + + // Try for a best match second, if using compact. + IntSize suggestedSize = SuggestedSize(aIdealKey.Size()); + if (suggestedSize != aIdealKey.Size()) { + if (!exactMatch) { + SurfaceKey compactKey = aIdealKey.CloneWithSize(suggestedSize); + mSurfaces.Get(compactKey, getter_AddRefs(exactMatch)); + if (exactMatch && exactMatch->IsDecoded()) { + MOZ_ASSERT(suggestedSize != aIdealKey.Size()); + return std::make_tuple(exactMatch.forget(), + MatchType::SUBSTITUTE_BECAUSE_BEST, + suggestedSize); + } + } + } + + // There's no perfect match, so find the best match we can. + RefPtr bestMatch; + for (const auto& value : Values()) { + NotNull current = WrapNotNull(value); + const SurfaceKey& currentKey = current->GetSurfaceKey(); + + // We never match a placeholder or a surface with a region. + if (current->IsPlaceholder() || currentKey.Region()) { + continue; + } + // Matching the playback type and SVG context is required. + if (currentKey.Playback() != aIdealKey.Playback() || + currentKey.SVGContext() != aIdealKey.SVGContext()) { + continue; + } + // Matching the flags is required. + if (currentKey.Flags() != aIdealKey.Flags()) { + continue; + } + // Anything is better than nothing! (Within the constraints we just + // checked, of course.) + if (!bestMatch) { + bestMatch = current; + continue; + } + + MOZ_ASSERT(bestMatch, "Should have a current best match"); + + // Always prefer completely decoded surfaces. + bool bestMatchIsDecoded = bestMatch->IsDecoded(); + if (bestMatchIsDecoded && !current->IsDecoded()) { + continue; + } + if (!bestMatchIsDecoded && current->IsDecoded()) { + bestMatch = current; + continue; + } + + SurfaceKey bestMatchKey = bestMatch->GetSurfaceKey(); + if (CompareArea(aIdealKey.Size(), bestMatchKey.Size(), + currentKey.Size())) { + bestMatch = current; + } + } + + MatchType matchType; + if (bestMatch) { + if (!exactMatch) { + // No exact match, neither ideal nor factor of 2. + MOZ_ASSERT(suggestedSize != bestMatch->GetSurfaceKey().Size(), + "No exact match despite the fact the sizes match!"); + matchType = MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND; + } else if (exactMatch != bestMatch) { + // The exact match is still decoding, but we found a substitute. + matchType = MatchType::SUBSTITUTE_BECAUSE_PENDING; + } else if (aIdealKey.Size() != bestMatch->GetSurfaceKey().Size()) { + // The best factor of 2 match is still decoding, but the best we've got. + MOZ_ASSERT(suggestedSize != aIdealKey.Size()); + MOZ_ASSERT(mFactor2Mode || mIsVectorImage); + matchType = MatchType::SUBSTITUTE_BECAUSE_BEST; + } else { + // The exact match is still decoding, but it's the best we've got. + matchType = MatchType::EXACT; + } + } else { + if (exactMatch) { + // We found an "exact match"; it must have been a placeholder. + MOZ_ASSERT(exactMatch->IsPlaceholder()); + matchType = MatchType::PENDING; + } else { + // We couldn't find an exact match *or* a substitute. + matchType = MatchType::NOT_FOUND; + } + } + + return std::make_tuple(bestMatch.forget(), matchType, suggestedSize); + } + + void MaybeSetFactor2Mode() { + MOZ_ASSERT(!mFactor2Mode); + + // Typically an image cache will not have too many size-varying surfaces, so + // if we exceed the given threshold, we should consider using a subset. + int32_t thresholdSurfaces = + StaticPrefs::image_cache_factor2_threshold_surfaces(); + if (thresholdSurfaces < 0 || + mSurfaces.Count() <= static_cast(thresholdSurfaces)) { + return; + } + + // Determine how many native surfaces this image has. If it is zero, and it + // is a vector image, then we should impute a single native size. Otherwise, + // it may be zero because we don't know yet, or the image has an error, or + // it isn't supported. + NotNull current = + WrapNotNull(mSurfaces.ConstIter().UserData()); + Image* image = static_cast(current->GetImageKey()); + size_t nativeSizes = image->GetNativeSizesLength(); + if (mIsVectorImage) { + MOZ_ASSERT(nativeSizes == 0); + nativeSizes = 1; + } else if (nativeSizes == 0) { + return; + } + + // Increase the threshold by the number of native sizes. This ensures that + // we do not prevent decoding of the image at all its native sizes. It does + // not guarantee we will provide a surface at that size however (i.e. many + // other sized surfaces are requested, in addition to the native sizes). + thresholdSurfaces += nativeSizes; + if (mSurfaces.Count() <= static_cast(thresholdSurfaces)) { + return; + } + + // We have a valid size, we can change modes. + mFactor2Mode = true; + } + + template + void Prune(Function&& aRemoveCallback) { + if (!mFactor2Mode || mFactor2Pruned) { + return; + } + + // Attempt to discard any surfaces which are not factor of 2 and the best + // factor of 2 match exists. + bool hasNotFactorSize = false; + for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) { + NotNull current = WrapNotNull(iter.UserData()); + const SurfaceKey& currentKey = current->GetSurfaceKey(); + const IntSize& currentSize = currentKey.Size(); + + // First we check if someone requested this size and would not accept + // an alternatively sized surface. + if (current->CannotSubstitute()) { + continue; + } + + // Next we find the best factor of 2 size for this surface. If this + // surface is a factor of 2 size, then we want to keep it. + IntSize bestSize = SuggestedSize(currentSize); + if (bestSize == currentSize) { + continue; + } + + // Check the cache for a surface with the same parameters except for the + // size which uses the closest factor of 2 size. + SurfaceKey compactKey = currentKey.CloneWithSize(bestSize); + RefPtr compactMatch; + mSurfaces.Get(compactKey, getter_AddRefs(compactMatch)); + if (compactMatch && compactMatch->IsDecoded()) { + aRemoveCallback(current); + iter.Remove(); + } else { + hasNotFactorSize = true; + } + } + + // We have no surfaces that are not factor of 2 sized, so we can stop + // pruning henceforth, because we avoid the insertion of new surfaces that + // don't match our sizing set (unless the caller won't accept a + // substitution.) + if (!hasNotFactorSize) { + mFactor2Pruned = true; + } + + // We should never leave factor of 2 mode due to pruning in of itself, but + // if we discarded surfaces due to the volatile buffers getting released, + // it is possible. + AfterMaybeRemove(); + } + + template + bool Invalidate(Function&& aRemoveCallback) { + // Remove all non-blob recordings from the cache. Invalidate any blob + // recordings. + bool foundRecording = false; + for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) { + NotNull current = WrapNotNull(iter.UserData()); + + if (current->GetSurfaceKey().Flags() & SurfaceFlags::RECORD_BLOB) { + foundRecording = true; + current->InvalidateRecording(); + continue; + } + + aRemoveCallback(current); + iter.Remove(); + } + + AfterMaybeRemove(); + return foundRecording; + } + + IntSize SuggestedSize(const IntSize& aSize) const { + IntSize suggestedSize = SuggestedSizeInternal(aSize); + if (mIsVectorImage) { + suggestedSize = SurfaceCache::ClampVectorSize(suggestedSize); + } + return suggestedSize; + } + + IntSize SuggestedSizeInternal(const IntSize& aSize) const { + // When not in factor of 2 mode, we can always decode at the given size. + if (!mFactor2Mode) { + return aSize; + } + + // We cannot enter factor of 2 mode unless we have a minimum number of + // surfaces, and we should have left it if the cache was emptied. + if (MOZ_UNLIKELY(IsEmpty())) { + MOZ_ASSERT_UNREACHABLE("Should not be empty and in factor of 2 mode!"); + return aSize; + } + + // This bit of awkwardness gets the largest native size of the image. + NotNull firstSurface = + WrapNotNull(mSurfaces.ConstIter().UserData()); + Image* image = static_cast(firstSurface->GetImageKey()); + IntSize factorSize; + if (NS_FAILED(image->GetWidth(&factorSize.width)) || + NS_FAILED(image->GetHeight(&factorSize.height)) || + factorSize.IsEmpty()) { + // Valid vector images may have a default size of 0x0. In that case, just + // assume a default size of 100x100 and apply the intrinsic ratio if + // available. If our guess was too small, don't use factor-of-scaling. + MOZ_ASSERT(mIsVectorImage); + factorSize = IntSize(100, 100); + Maybe aspectRatio = image->GetIntrinsicRatio(); + if (aspectRatio && *aspectRatio) { + factorSize.width = + NSToIntRound(aspectRatio->ApplyToFloat(float(factorSize.height))); + if (factorSize.IsEmpty()) { + return aSize; + } + } + } + + if (mIsVectorImage) { + // Ensure the aspect ratio matches the native size before forcing the + // caller to accept a factor of 2 size. The difference between the aspect + // ratios is: + // + // delta = nativeWidth/nativeHeight - desiredWidth/desiredHeight + // + // delta*nativeHeight*desiredHeight = nativeWidth*desiredHeight + // - desiredWidth*nativeHeight + // + // Using the maximum accepted delta as a constant, we can avoid the + // floating point division and just compare after some integer ops. + int32_t delta = + factorSize.width * aSize.height - aSize.width * factorSize.height; + int32_t maxDelta = (factorSize.height * aSize.height) >> 4; + if (delta > maxDelta || delta < -maxDelta) { + return aSize; + } + + // If the requested size is bigger than the native size, we actually need + // to grow the native size instead of shrinking it. + if (factorSize.width < aSize.width) { + do { + IntSize candidate(factorSize.width * 2, factorSize.height * 2); + if (!SurfaceCache::IsLegalSize(candidate)) { + break; + } + + factorSize = candidate; + } while (factorSize.width < aSize.width); + + return factorSize; + } + + // Otherwise we can find the best fit as normal. + } + + // Start with the native size as the best first guess. + IntSize bestSize = factorSize; + factorSize.width /= 2; + factorSize.height /= 2; + + while (!factorSize.IsEmpty()) { + if (!CompareArea(aSize, bestSize, factorSize)) { + // This size is not better than the last. Since we proceed from largest + // to smallest, we know that the next size will not be better if the + // previous size was rejected. Break early. + break; + } + + // The current factor of 2 size is better than the last selected size. + bestSize = factorSize; + factorSize.width /= 2; + factorSize.height /= 2; + } + + return bestSize; + } + + bool CompareArea(const IntSize& aIdealSize, const IntSize& aBestSize, + const IntSize& aSize) const { + // Compare sizes. We use an area-based heuristic here instead of computing a + // truly optimal answer, since it seems very unlikely to make a difference + // for realistic sizes. + int64_t idealArea = AreaOfIntSize(aIdealSize); + int64_t currentArea = AreaOfIntSize(aSize); + int64_t bestMatchArea = AreaOfIntSize(aBestSize); + + // If the best match is smaller than the ideal size, prefer bigger sizes. + if (bestMatchArea < idealArea) { + if (currentArea > bestMatchArea) { + return true; + } + return false; + } + + // Other, prefer sizes closer to the ideal size, but still not smaller. + if (idealArea <= currentArea && currentArea < bestMatchArea) { + return true; + } + + // This surface isn't an improvement over the current best match. + return false; + } + + template + void CollectSizeOfSurfaces(nsTArray& aCounters, + MallocSizeOf aMallocSizeOf, + Function&& aRemoveCallback) { + CachedSurface::SurfaceMemoryReport report(aCounters, aMallocSizeOf); + for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) { + NotNull surface = WrapNotNull(iter.UserData()); + + // We don't need the drawable surface for ourselves, but adding a surface + // to the report will trigger this indirectly. If the surface was + // discarded by the OS because it was in volatile memory, we should remove + // it from the cache immediately rather than include it in the report. + DrawableSurface drawableSurface; + if (!surface->IsPlaceholder()) { + drawableSurface = surface->GetDrawableSurface(); + if (!drawableSurface) { + aRemoveCallback(surface); + iter.Remove(); + continue; + } + } + + const IntSize& size = surface->GetSurfaceKey().Size(); + bool factor2Size = false; + if (mFactor2Mode) { + factor2Size = (size == SuggestedSize(size)); + } + report.Add(surface, factor2Size); + } + + AfterMaybeRemove(); + } + + void SetLocked(bool aLocked) { mLocked = aLocked; } + bool IsLocked() const { return mLocked; } + + private: + void AfterMaybeRemove() { + if (IsEmpty() && mFactor2Mode) { + // The last surface for this cache was removed. This can happen if the + // surface was stored in a volatile buffer and got purged, or the surface + // expired from the cache. If the cache itself lingers for some reason + // (e.g. in the process of performing a lookup, the cache itself is + // locked), then we need to reset the factor of 2 state because it + // requires at least one surface present to get the native size + // information from the image. + mFactor2Mode = mFactor2Pruned = false; + } + } + + SurfaceTable mSurfaces; + + bool mLocked; + + // True in "factor of 2" mode. + bool mFactor2Mode; + + // True if all non-factor of 2 surfaces have been removed from the cache. Note + // that this excludes unsubstitutable sizes. + bool mFactor2Pruned; + + // True if the surfaces are produced from a vector image. If so, it must match + // the aspect ratio when using factor of 2 mode. + bool mIsVectorImage; +}; + +/** + * SurfaceCacheImpl is responsible for determining which surfaces will be cached + * and managing the surface cache data structures. Rather than interact with + * SurfaceCacheImpl directly, client code interacts with SurfaceCache, which + * maintains high-level invariants and encapsulates the details of the surface + * cache's implementation. + */ +class SurfaceCacheImpl final : public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + + SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS, + uint32_t aSurfaceCacheDiscardFactor, + uint32_t aSurfaceCacheSize) + : mExpirationTracker(aSurfaceCacheExpirationTimeMS), + mMemoryPressureObserver(new MemoryPressureObserver), + mDiscardFactor(aSurfaceCacheDiscardFactor), + mMaxCost(aSurfaceCacheSize), + mAvailableCost(aSurfaceCacheSize), + mLockedCost(0), + mOverflowCount(0), + mAlreadyPresentCount(0), + mTableFailureCount(0), + mTrackingFailureCount(0) { + nsCOMPtr os = services::GetObserverService(); + if (os) { + os->AddObserver(mMemoryPressureObserver, "memory-pressure", false); + } + } + + private: + virtual ~SurfaceCacheImpl() { + nsCOMPtr os = services::GetObserverService(); + if (os) { + os->RemoveObserver(mMemoryPressureObserver, "memory-pressure"); + } + + UnregisterWeakMemoryReporter(this); + } + + public: + void InitMemoryReporter() { RegisterWeakMemoryReporter(this); } + + InsertOutcome Insert(NotNull aProvider, bool aSetAvailable, + const StaticMutexAutoLock& aAutoLock) { + // If this is a duplicate surface, refuse to replace the original. + // XXX(seth): Calling Lookup() and then RemoveEntry() does the lookup + // twice. We'll make this more efficient in bug 1185137. + LookupResult result = + Lookup(aProvider->GetImageKey(), aProvider->GetSurfaceKey(), aAutoLock, + /* aMarkUsed = */ false); + if (MOZ_UNLIKELY(result)) { + mAlreadyPresentCount++; + return InsertOutcome::FAILURE_ALREADY_PRESENT; + } + + if (result.Type() == MatchType::PENDING) { + RemoveEntry(aProvider->GetImageKey(), aProvider->GetSurfaceKey(), + aAutoLock); + } + + MOZ_ASSERT(result.Type() == MatchType::NOT_FOUND || + result.Type() == MatchType::PENDING, + "A LookupResult with no surface should be NOT_FOUND or PENDING"); + + // If this is bigger than we can hold after discarding everything we can, + // refuse to cache it. + Cost cost = aProvider->LogicalSizeInBytes(); + if (MOZ_UNLIKELY(!CanHoldAfterDiscarding(cost))) { + mOverflowCount++; + return InsertOutcome::FAILURE; + } + + // Remove elements in order of cost until we can fit this in the cache. Note + // that locked surfaces aren't in mCosts, so we never remove them here. + while (cost > mAvailableCost) { + MOZ_ASSERT(!mCosts.IsEmpty(), + "Removed everything and it still won't fit"); + Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true, + aAutoLock); + } + + // Locate the appropriate per-image cache. If there's not an existing cache + // for this image, create it. + const ImageKey imageKey = aProvider->GetImageKey(); + RefPtr cache = GetImageCache(imageKey); + if (!cache) { + cache = new ImageSurfaceCache(imageKey); + if (!mImageCaches.InsertOrUpdate(aProvider->GetImageKey(), RefPtr{cache}, + fallible)) { + mTableFailureCount++; + return InsertOutcome::FAILURE; + } + } + + // If we were asked to mark the cache entry available, do so. + if (aSetAvailable) { + aProvider->Availability().SetAvailable(); + } + + auto surface = MakeNotNull>(aProvider); + + // We require that locking succeed if the image is locked and we're not + // inserting a placeholder; the caller may need to know this to handle + // errors correctly. + bool mustLock = cache->IsLocked() && !surface->IsPlaceholder(); + if (mustLock) { + surface->SetLocked(true); + if (!surface->IsLocked()) { + return InsertOutcome::FAILURE; + } + } + + // Insert. + MOZ_ASSERT(cost <= mAvailableCost, "Inserting despite too large a cost"); + if (!cache->Insert(surface)) { + mTableFailureCount++; + if (mustLock) { + surface->SetLocked(false); + } + return InsertOutcome::FAILURE; + } + + if (MOZ_UNLIKELY(!StartTracking(surface, aAutoLock))) { + MOZ_ASSERT(!mustLock); + Remove(surface, /* aStopTracking */ false, aAutoLock); + return InsertOutcome::FAILURE; + } + + return InsertOutcome::SUCCESS; + } + + void Remove(NotNull aSurface, bool aStopTracking, + const StaticMutexAutoLock& aAutoLock) { + ImageKey imageKey = aSurface->GetImageKey(); + + RefPtr cache = GetImageCache(imageKey); + MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache"); + + // If the surface was not a placeholder, tell its image that we discarded + // it. + if (!aSurface->IsPlaceholder()) { + static_cast(imageKey)->OnSurfaceDiscarded( + aSurface->GetSurfaceKey()); + } + + // If we failed during StartTracking, we can skip this step. + if (aStopTracking) { + StopTracking(aSurface, /* aIsTracked */ true, aAutoLock); + } + + // Individual surfaces must be freed outside the lock. + mCachedSurfacesDiscard.AppendElement(cache->Remove(aSurface)); + + MaybeRemoveEmptyCache(imageKey, cache); + } + + bool StartTracking(NotNull aSurface, + const StaticMutexAutoLock& aAutoLock) { + CostEntry costEntry = aSurface->GetCostEntry(); + MOZ_ASSERT(costEntry.GetCost() <= mAvailableCost, + "Cost too large and the caller didn't catch it"); + + if (aSurface->IsLocked()) { + mLockedCost += costEntry.GetCost(); + MOZ_ASSERT(mLockedCost <= mMaxCost, "Locked more than we can hold?"); + } else { + if (NS_WARN_IF(!mCosts.InsertElementSorted(costEntry, fallible))) { + mTrackingFailureCount++; + return false; + } + + // This may fail during XPCOM shutdown, so we need to ensure the object is + // tracked before calling RemoveObject in StopTracking. + nsresult rv = mExpirationTracker.AddObjectLocked(aSurface, aAutoLock); + if (NS_WARN_IF(NS_FAILED(rv))) { + DebugOnly foundInCosts = mCosts.RemoveElementSorted(costEntry); + MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface"); + mTrackingFailureCount++; + return false; + } + } + + mAvailableCost -= costEntry.GetCost(); + return true; + } + + void StopTracking(NotNull aSurface, bool aIsTracked, + const StaticMutexAutoLock& aAutoLock) { + CostEntry costEntry = aSurface->GetCostEntry(); + + if (aSurface->IsLocked()) { + MOZ_ASSERT(mLockedCost >= costEntry.GetCost(), "Costs don't balance"); + mLockedCost -= costEntry.GetCost(); + // XXX(seth): It'd be nice to use an O(log n) lookup here. This is O(n). + MOZ_ASSERT(!mCosts.Contains(costEntry), + "Shouldn't have a cost entry for a locked surface"); + } else { + if (MOZ_LIKELY(aSurface->GetExpirationState()->IsTracked())) { + MOZ_ASSERT(aIsTracked, "Expiration-tracking a surface unexpectedly!"); + mExpirationTracker.RemoveObjectLocked(aSurface, aAutoLock); + } else { + // Our call to AddObject must have failed in StartTracking; most likely + // we're in XPCOM shutdown right now. + MOZ_ASSERT(!aIsTracked, "Not expiration-tracking an unlocked surface!"); + } + + DebugOnly foundInCosts = mCosts.RemoveElementSorted(costEntry); + MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface"); + } + + mAvailableCost += costEntry.GetCost(); + MOZ_ASSERT(mAvailableCost <= mMaxCost, + "More available cost than we started with"); + } + + LookupResult Lookup(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + const StaticMutexAutoLock& aAutoLock, bool aMarkUsed) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + // No cached surfaces for this image. + return LookupResult(MatchType::NOT_FOUND); + } + + RefPtr surface = cache->Lookup(aSurfaceKey, aMarkUsed); + if (!surface) { + // Lookup in the per-image cache missed. + return LookupResult(MatchType::NOT_FOUND); + } + + if (surface->IsPlaceholder()) { + return LookupResult(MatchType::PENDING); + } + + DrawableSurface drawableSurface = surface->GetDrawableSurface(); + if (!drawableSurface) { + // The surface was released by the operating system. Remove the cache + // entry as well. + Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock); + return LookupResult(MatchType::NOT_FOUND); + } + + if (aMarkUsed && + !MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock)) { + Remove(WrapNotNull(surface), /* aStopTracking */ false, aAutoLock); + return LookupResult(MatchType::NOT_FOUND); + } + + MOZ_ASSERT(surface->GetSurfaceKey() == aSurfaceKey, + "Lookup() not returning an exact match?"); + return LookupResult(std::move(drawableSurface), MatchType::EXACT); + } + + LookupResult LookupBestMatch(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey, + const StaticMutexAutoLock& aAutoLock, + bool aMarkUsed) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + // No cached surfaces for this image. + return LookupResult( + MatchType::NOT_FOUND, + SurfaceCache::ClampSize(aImageKey, aSurfaceKey.Size())); + } + + // Repeatedly look up the best match, trying again if the resulting surface + // has been freed by the operating system, until we can either lock a + // surface for drawing or there are no matching surfaces left. + // XXX(seth): This is O(N^2), but N is expected to be very small. If we + // encounter a performance problem here we can revisit this. + + RefPtr surface; + DrawableSurface drawableSurface; + MatchType matchType = MatchType::NOT_FOUND; + IntSize suggestedSize; + while (true) { + std::tie(surface, matchType, suggestedSize) = + cache->LookupBestMatch(aSurfaceKey); + + if (!surface) { + return LookupResult( + matchType, suggestedSize); // Lookup in the per-image cache missed. + } + + drawableSurface = surface->GetDrawableSurface(); + if (drawableSurface) { + break; + } + + // The surface was released by the operating system. Remove the cache + // entry as well. + Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock); + } + + MOZ_ASSERT_IF(matchType == MatchType::EXACT, + surface->GetSurfaceKey() == aSurfaceKey); + MOZ_ASSERT_IF( + matchType == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND || + matchType == MatchType::SUBSTITUTE_BECAUSE_PENDING, + surface->GetSurfaceKey().Region() == aSurfaceKey.Region() && + surface->GetSurfaceKey().SVGContext() == aSurfaceKey.SVGContext() && + surface->GetSurfaceKey().Playback() == aSurfaceKey.Playback() && + surface->GetSurfaceKey().Flags() == aSurfaceKey.Flags()); + + if (matchType == MatchType::EXACT || + matchType == MatchType::SUBSTITUTE_BECAUSE_BEST) { + if (aMarkUsed && + !MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock)) { + Remove(WrapNotNull(surface), /* aStopTracking */ false, aAutoLock); + } + } + + return LookupResult(std::move(drawableSurface), matchType, suggestedSize); + } + + bool CanHold(const Cost aCost) const { return aCost <= mMaxCost; } + + size_t MaximumCapacity() const { return size_t(mMaxCost); } + + void SurfaceAvailable(NotNull aProvider, + const StaticMutexAutoLock& aAutoLock) { + if (!aProvider->Availability().IsPlaceholder()) { + MOZ_ASSERT_UNREACHABLE("Calling SurfaceAvailable on non-placeholder"); + return; + } + + // Reinsert the provider, requesting that Insert() mark it available. This + // may or may not succeed, depending on whether some other decoder has + // beaten us to the punch and inserted a non-placeholder version of this + // surface first, but it's fine either way. + // XXX(seth): This could be implemented more efficiently; we should be able + // to just update our data structures without reinserting. + Insert(aProvider, /* aSetAvailable = */ true, aAutoLock); + } + + void LockImage(const ImageKey aImageKey) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + cache = new ImageSurfaceCache(aImageKey); + mImageCaches.InsertOrUpdate(aImageKey, RefPtr{cache}); + } + + cache->SetLocked(true); + + // We don't relock this image's existing surfaces right away; instead, the + // image should arrange for Lookup() to touch them if they are still useful. + } + + void UnlockImage(const ImageKey aImageKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache || !cache->IsLocked()) { + return; // Already unlocked. + } + + cache->SetLocked(false); + DoUnlockSurfaces(WrapNotNull(cache), /* aStaticOnly = */ false, aAutoLock); + } + + void UnlockEntries(const ImageKey aImageKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache || !cache->IsLocked()) { + return; // Already unlocked. + } + + // (Note that we *don't* unlock the per-image cache here; that's the + // difference between this and UnlockImage.) + DoUnlockSurfaces(WrapNotNull(cache), + /* aStaticOnly = */ + !StaticPrefs::image_mem_animated_discardable_AtStartup(), + aAutoLock); + } + + already_AddRefed RemoveImage( + const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + return nullptr; // No cached surfaces for this image, so nothing to do. + } + + // Discard all of the cached surfaces for this image. + // XXX(seth): This is O(n^2) since for each item in the cache we are + // removing an element from the costs array. Since n is expected to be + // small, performance should be good, but if usage patterns change we should + // change the data structure used for mCosts. + for (const auto& value : cache->Values()) { + StopTracking(WrapNotNull(value), + /* aIsTracked */ true, aAutoLock); + } + + // The per-image cache isn't needed anymore, so remove it as well. + // This implicitly unlocks the image if it was locked. + mImageCaches.Remove(aImageKey); + + // Since we did not actually remove any of the surfaces from the cache + // itself, only stopped tracking them, we should free it outside the lock. + return cache.forget(); + } + + void PruneImage(const ImageKey aImageKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + return; // No cached surfaces for this image, so nothing to do. + } + + cache->Prune([this, &aAutoLock](NotNull aSurface) -> void { + StopTracking(aSurface, /* aIsTracked */ true, aAutoLock); + // Individual surfaces must be freed outside the lock. + mCachedSurfacesDiscard.AppendElement(aSurface); + }); + + MaybeRemoveEmptyCache(aImageKey, cache); + } + + bool InvalidateImage(const ImageKey aImageKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + return false; // No cached surfaces for this image, so nothing to do. + } + + bool rv = cache->Invalidate( + [this, &aAutoLock](NotNull aSurface) -> void { + StopTracking(aSurface, /* aIsTracked */ true, aAutoLock); + // Individual surfaces must be freed outside the lock. + mCachedSurfacesDiscard.AppendElement(aSurface); + }); + + MaybeRemoveEmptyCache(aImageKey, cache); + return rv; + } + + void DiscardAll(const StaticMutexAutoLock& aAutoLock) { + // Remove in order of cost because mCosts is an array and the other data + // structures are all hash tables. Note that locked surfaces are not + // removed, since they aren't present in mCosts. + while (!mCosts.IsEmpty()) { + Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true, + aAutoLock); + } + } + + void DiscardForMemoryPressure(const StaticMutexAutoLock& aAutoLock) { + // Compute our discardable cost. Since locked surfaces aren't discardable, + // we exclude them. + const Cost discardableCost = (mMaxCost - mAvailableCost) - mLockedCost; + MOZ_ASSERT(discardableCost <= mMaxCost, "Discardable cost doesn't add up"); + + // Our target is to raise our available cost by (1 / mDiscardFactor) of our + // discardable cost - in other words, we want to end up with about + // (discardableCost / mDiscardFactor) fewer bytes stored in the surface + // cache after we're done. + const Cost targetCost = mAvailableCost + (discardableCost / mDiscardFactor); + + if (targetCost > mMaxCost - mLockedCost) { + MOZ_ASSERT_UNREACHABLE("Target cost is more than we can discard"); + DiscardAll(aAutoLock); + return; + } + + // Discard surfaces until we've reduced our cost to our target cost. + while (mAvailableCost < targetCost) { + MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and still not done"); + Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true, + aAutoLock); + } + } + + void TakeDiscard(nsTArray>& aDiscard, + const StaticMutexAutoLock& aAutoLock) { + MOZ_ASSERT(aDiscard.IsEmpty()); + aDiscard = std::move(mCachedSurfacesDiscard); + } + + already_AddRefed GetSurfaceForResetAnimation( + const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr surface; + + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + // No cached surfaces for this image. + return surface.forget(); + } + + surface = cache->Lookup(aSurfaceKey, /* aForAccess = */ false); + return surface.forget(); + } + + void LockSurface(NotNull aSurface, + const StaticMutexAutoLock& aAutoLock) { + if (aSurface->IsPlaceholder() || aSurface->IsLocked()) { + return; + } + + StopTracking(aSurface, /* aIsTracked */ true, aAutoLock); + + // Lock the surface. This can fail. + aSurface->SetLocked(true); + DebugOnly tracked = StartTracking(aSurface, aAutoLock); + MOZ_ASSERT(tracked); + } + + size_t ShallowSizeOfIncludingThis( + MallocSizeOf aMallocSizeOf, const StaticMutexAutoLock& aAutoLock) const { + size_t bytes = + aMallocSizeOf(this) + mCosts.ShallowSizeOfExcludingThis(aMallocSizeOf) + + mImageCaches.ShallowSizeOfExcludingThis(aMallocSizeOf) + + mCachedSurfacesDiscard.ShallowSizeOfExcludingThis(aMallocSizeOf) + + mExpirationTracker.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& data : mImageCaches.Values()) { + bytes += data->ShallowSizeOfIncludingThis(aMallocSizeOf); + } + return bytes; + } + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) override { + StaticMutexAutoLock lock(sInstanceMutex); + + uint32_t lockedImageCount = 0; + uint32_t totalSurfaceCount = 0; + uint32_t lockedSurfaceCount = 0; + for (const auto& cache : mImageCaches.Values()) { + totalSurfaceCount += cache->Count(); + if (cache->IsLocked()) { + ++lockedImageCount; + } + for (const auto& value : cache->Values()) { + if (value->IsLocked()) { + ++lockedSurfaceCount; + } + } + } + + // clang-format off + // We have explicit memory reporting for the surface cache which is more + // accurate than the cost metrics we report here, but these metrics are + // still useful to report, since they control the cache's behavior. + MOZ_COLLECT_REPORT( + "explicit/images/cache/overhead", KIND_HEAP, UNITS_BYTES, + ShallowSizeOfIncludingThis(SurfaceCacheMallocSizeOf, lock), +"Memory used by the surface cache data structures, excluding surface data."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-estimated-total", + KIND_OTHER, UNITS_BYTES, (mMaxCost - mAvailableCost), +"Estimated total memory used by the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-estimated-locked", + KIND_OTHER, UNITS_BYTES, mLockedCost, +"Estimated memory used by locked surfaces in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-tracked-cost-count", + KIND_OTHER, UNITS_COUNT, mCosts.Length(), +"Total number of surfaces tracked for cost (and expiry) in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-tracked-expiry-count", + KIND_OTHER, UNITS_COUNT, mExpirationTracker.Length(lock), +"Total number of surfaces tracked for expiry (and cost) in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-image-count", + KIND_OTHER, UNITS_COUNT, mImageCaches.Count(), +"Total number of images in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-locked-image-count", + KIND_OTHER, UNITS_COUNT, lockedImageCount, +"Total number of locked images in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-image-surface-count", + KIND_OTHER, UNITS_COUNT, totalSurfaceCount, +"Total number of surfaces in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-locked-surfaces-count", + KIND_OTHER, UNITS_COUNT, lockedSurfaceCount, +"Total number of locked surfaces in the imagelib surface cache."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-overflow-count", + KIND_OTHER, UNITS_COUNT, mOverflowCount, +"Count of how many times the surface cache has hit its capacity and been " +"unable to insert a new surface."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-tracking-failure-count", + KIND_OTHER, UNITS_COUNT, mTrackingFailureCount, +"Count of how many times the surface cache has failed to begin tracking a " +"given surface."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-already-present-count", + KIND_OTHER, UNITS_COUNT, mAlreadyPresentCount, +"Count of how many times the surface cache has failed to insert a surface " +"because it is already present."); + + MOZ_COLLECT_REPORT( + "imagelib-surface-cache-table-failure-count", + KIND_OTHER, UNITS_COUNT, mTableFailureCount, +"Count of how many times the surface cache has failed to insert a surface " +"because a hash table could not accept an entry."); + // clang-format on + + return NS_OK; + } + + void CollectSizeOfSurfaces(const ImageKey aImageKey, + nsTArray& aCounters, + MallocSizeOf aMallocSizeOf, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + return; // No surfaces for this image. + } + + // Report all surfaces in the per-image cache. + cache->CollectSizeOfSurfaces( + aCounters, aMallocSizeOf, + [this, &aAutoLock](NotNull aSurface) -> void { + StopTracking(aSurface, /* aIsTracked */ true, aAutoLock); + // Individual surfaces must be freed outside the lock. + mCachedSurfacesDiscard.AppendElement(aSurface); + }); + + MaybeRemoveEmptyCache(aImageKey, cache); + } + + void ReleaseImageOnMainThread(already_AddRefed&& aImage, + const StaticMutexAutoLock& aAutoLock) { + RefPtr image = aImage; + if (!image) { + return; + } + + bool needsDispatch = mReleasingImagesOnMainThread.IsEmpty(); + mReleasingImagesOnMainThread.AppendElement(image); + + if (!needsDispatch || + AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownFinal)) { + // Either there is already a ongoing task for ClearReleasingImages() or + // it's too late in shutdown to dispatch. + return; + } + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SurfaceCacheImpl::ReleaseImageOnMainThread", + []() -> void { SurfaceCache::ClearReleasingImages(); })); + } + + void TakeReleasingImages(nsTArray>& aImage, + const StaticMutexAutoLock& aAutoLock) { + MOZ_ASSERT(NS_IsMainThread()); + aImage.SwapElements(mReleasingImagesOnMainThread); + } + + private: + already_AddRefed GetImageCache(const ImageKey aImageKey) { + RefPtr imageCache; + mImageCaches.Get(aImageKey, getter_AddRefs(imageCache)); + return imageCache.forget(); + } + + void MaybeRemoveEmptyCache(const ImageKey aImageKey, + ImageSurfaceCache* aCache) { + // Remove the per-image cache if it's unneeded now. Keep it if the image is + // locked, since the per-image cache is where we store that state. Note that + // we don't push it into mImageCachesDiscard because all of its surfaces + // have been removed, so it is safe to free while holding the lock. + if (aCache->IsEmpty() && !aCache->IsLocked()) { + mImageCaches.Remove(aImageKey); + } + } + + // This is similar to CanHold() except that it takes into account the costs of + // locked surfaces. It's used internally in Insert(), but it's not exposed + // publicly because we permit multithreaded access to the surface cache, which + // means that the result would be meaningless: another thread could insert a + // surface or lock an image at any time. + bool CanHoldAfterDiscarding(const Cost aCost) const { + return aCost <= mMaxCost - mLockedCost; + } + + bool MarkUsed(NotNull aSurface, + NotNull aCache, + const StaticMutexAutoLock& aAutoLock) { + if (aCache->IsLocked()) { + LockSurface(aSurface, aAutoLock); + return true; + } + + nsresult rv = mExpirationTracker.MarkUsedLocked(aSurface, aAutoLock); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If mark used fails, it is because it failed to reinsert the surface + // after removing it from the tracker. Thus we need to update our + // own accounting but otherwise expect it to be untracked. + StopTracking(aSurface, /* aIsTracked */ false, aAutoLock); + return false; + } + return true; + } + + void DoUnlockSurfaces(NotNull aCache, bool aStaticOnly, + const StaticMutexAutoLock& aAutoLock) { + AutoTArray, 8> discard; + + // Unlock all the surfaces the per-image cache is holding. + for (const auto& value : aCache->Values()) { + NotNull surface = WrapNotNull(value); + if (surface->IsPlaceholder() || !surface->IsLocked()) { + continue; + } + if (aStaticOnly && + surface->GetSurfaceKey().Playback() != PlaybackType::eStatic) { + continue; + } + StopTracking(surface, /* aIsTracked */ true, aAutoLock); + surface->SetLocked(false); + if (MOZ_UNLIKELY(!StartTracking(surface, aAutoLock))) { + discard.AppendElement(surface); + } + } + + // Discard any that we failed to track. + for (auto iter = discard.begin(); iter != discard.end(); ++iter) { + Remove(*iter, /* aStopTracking */ false, aAutoLock); + } + } + + void RemoveEntry(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey, + const StaticMutexAutoLock& aAutoLock) { + RefPtr cache = GetImageCache(aImageKey); + if (!cache) { + return; // No cached surfaces for this image. + } + + RefPtr surface = + cache->Lookup(aSurfaceKey, /* aForAccess = */ false); + if (!surface) { + return; // Lookup in the per-image cache missed. + } + + Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock); + } + + class SurfaceTracker final + : public ExpirationTrackerImpl { + public: + explicit SurfaceTracker(uint32_t aSurfaceCacheExpirationTimeMS) + : ExpirationTrackerImpl( + aSurfaceCacheExpirationTimeMS, "SurfaceTracker") {} + + protected: + void NotifyExpiredLocked(CachedSurface* aSurface, + const StaticMutexAutoLock& aAutoLock) override { + sInstance->Remove(WrapNotNull(aSurface), /* aStopTracking */ true, + aAutoLock); + } + + void NotifyHandlerEndLocked(const StaticMutexAutoLock& aAutoLock) override { + sInstance->TakeDiscard(mDiscard, aAutoLock); + } + + void NotifyHandlerEnd() override { + nsTArray> discard(std::move(mDiscard)); + } + + StaticMutex& GetMutex() override { return sInstanceMutex; } + + nsTArray> mDiscard; + }; + + class MemoryPressureObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + nsTArray> discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance && strcmp(aTopic, "memory-pressure") == 0) { + sInstance->DiscardForMemoryPressure(lock); + sInstance->TakeDiscard(discard, lock); + } + } + return NS_OK; + } + + private: + virtual ~MemoryPressureObserver() {} + }; + + nsTArray mCosts; + nsRefPtrHashtable, ImageSurfaceCache> mImageCaches; + nsTArray> mCachedSurfacesDiscard; + SurfaceTracker mExpirationTracker; + RefPtr mMemoryPressureObserver; + nsTArray> mReleasingImagesOnMainThread; + const uint32_t mDiscardFactor; + const Cost mMaxCost; + Cost mAvailableCost; + Cost mLockedCost; + size_t mOverflowCount; + size_t mAlreadyPresentCount; + size_t mTableFailureCount; + size_t mTrackingFailureCount; +}; + +NS_IMPL_ISUPPORTS(SurfaceCacheImpl, nsIMemoryReporter) +NS_IMPL_ISUPPORTS(SurfaceCacheImpl::MemoryPressureObserver, nsIObserver) + +/////////////////////////////////////////////////////////////////////////////// +// Public API +/////////////////////////////////////////////////////////////////////////////// + +/* static */ +void SurfaceCache::Initialize() { + // Initialize preferences. + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once"); + + // See StaticPrefs for the default values of these preferences. + + // Length of time before an unused surface is removed from the cache, in + // milliseconds. + uint32_t surfaceCacheExpirationTimeMS = + StaticPrefs::image_mem_surfacecache_min_expiration_ms_AtStartup(); + + // What fraction of the memory used by the surface cache we should discard + // when we get a memory pressure notification. This value is interpreted as + // 1/N, so 1 means to discard everything, 2 means to discard about half of the + // memory we're using, and so forth. We clamp it to avoid division by zero. + uint32_t surfaceCacheDiscardFactor = + max(StaticPrefs::image_mem_surfacecache_discard_factor_AtStartup(), 1u); + + // Maximum size of the surface cache, in kilobytes. + uint64_t surfaceCacheMaxSizeKB = + StaticPrefs::image_mem_surfacecache_max_size_kb_AtStartup(); + + if (sizeof(uintptr_t) <= 4) { + // Limit surface cache to 1 GB if our address space is 32 bit. + surfaceCacheMaxSizeKB = 1024 * 1024; + } + + // A knob determining the actual size of the surface cache. Currently the + // cache is (size of main memory) / (surface cache size factor) KB + // or (surface cache max size) KB, whichever is smaller. The formula + // may change in the future, though. + // For example, a value of 4 would yield a 256MB cache on a 1GB machine. + // The smallest machines we are likely to run this code on have 256MB + // of memory, which would yield a 64MB cache on this setting. + // We clamp this value to avoid division by zero. + uint32_t surfaceCacheSizeFactor = + max(StaticPrefs::image_mem_surfacecache_size_factor_AtStartup(), 1u); + + // Compute the size of the surface cache. + uint64_t memorySize = PR_GetPhysicalMemorySize(); + if (memorySize == 0) { +#if !defined(__DragonFly__) + MOZ_ASSERT_UNREACHABLE("PR_GetPhysicalMemorySize not implemented here"); +#endif + memorySize = 256 * 1024 * 1024; // Fall back to 256MB. + } + uint64_t proposedSize = memorySize / surfaceCacheSizeFactor; + uint64_t surfaceCacheSizeBytes = + min(proposedSize, surfaceCacheMaxSizeKB * 1024); + uint32_t finalSurfaceCacheSizeBytes = + min(surfaceCacheSizeBytes, uint64_t(UINT32_MAX)); + + // Create the surface cache singleton with the requested settings. Note that + // the size is a limit that the cache may not grow beyond, but we do not + // actually allocate any storage for surfaces at this time. + sInstance = new SurfaceCacheImpl(surfaceCacheExpirationTimeMS, + surfaceCacheDiscardFactor, + finalSurfaceCacheSizeBytes); + sInstance->InitMemoryReporter(); +} + +/* static */ +void SurfaceCache::Shutdown() { + RefPtr cache; + { + StaticMutexAutoLock lock(sInstanceMutex); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?"); + cache = sInstance.forget(); + } +} + +/* static */ +LookupResult SurfaceCache::Lookup(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey, + bool aMarkUsed) { + nsTArray> discard; + LookupResult rv(MatchType::NOT_FOUND); + + { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return rv; + } + + rv = sInstance->Lookup(aImageKey, aSurfaceKey, lock, aMarkUsed); + sInstance->TakeDiscard(discard, lock); + } + + return rv; +} + +/* static */ +LookupResult SurfaceCache::LookupBestMatch(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey, + bool aMarkUsed) { + nsTArray> discard; + LookupResult rv(MatchType::NOT_FOUND); + + { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return rv; + } + + rv = sInstance->LookupBestMatch(aImageKey, aSurfaceKey, lock, aMarkUsed); + sInstance->TakeDiscard(discard, lock); + } + + return rv; +} + +/* static */ +InsertOutcome SurfaceCache::Insert(NotNull aProvider) { + nsTArray> discard; + InsertOutcome rv(InsertOutcome::FAILURE); + + { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return rv; + } + + rv = sInstance->Insert(aProvider, /* aSetAvailable = */ false, lock); + sInstance->TakeDiscard(discard, lock); + } + + return rv; +} + +/* static */ +bool SurfaceCache::CanHold(const IntSize& aSize, + uint32_t aBytesPerPixel /* = 4 */) { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return false; + } + + Cost cost = ComputeCost(aSize, aBytesPerPixel); + return sInstance->CanHold(cost); +} + +/* static */ +bool SurfaceCache::CanHold(size_t aSize) { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return false; + } + + return sInstance->CanHold(aSize); +} + +/* static */ +void SurfaceCache::SurfaceAvailable(NotNull aProvider) { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return; + } + + sInstance->SurfaceAvailable(aProvider, lock); +} + +/* static */ +void SurfaceCache::LockImage(const ImageKey aImageKey) { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + return sInstance->LockImage(aImageKey); + } +} + +/* static */ +void SurfaceCache::UnlockImage(const ImageKey aImageKey) { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + return sInstance->UnlockImage(aImageKey, lock); + } +} + +/* static */ +void SurfaceCache::UnlockEntries(const ImageKey aImageKey) { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + return sInstance->UnlockEntries(aImageKey, lock); + } +} + +/* static */ +void SurfaceCache::RemoveImage(const ImageKey aImageKey) { + RefPtr discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + discard = sInstance->RemoveImage(aImageKey, lock); + } + } +} + +/* static */ +void SurfaceCache::PruneImage(const ImageKey aImageKey) { + nsTArray> discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + sInstance->PruneImage(aImageKey, lock); + sInstance->TakeDiscard(discard, lock); + } + } +} + +/* static */ +bool SurfaceCache::InvalidateImage(const ImageKey aImageKey) { + nsTArray> discard; + bool rv = false; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + rv = sInstance->InvalidateImage(aImageKey, lock); + sInstance->TakeDiscard(discard, lock); + } + } + return rv; +} + +/* static */ +void SurfaceCache::DiscardAll() { + nsTArray> discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + sInstance->DiscardAll(lock); + sInstance->TakeDiscard(discard, lock); + } + } +} + +/* static */ +void SurfaceCache::ResetAnimation(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey) { + RefPtr surface; + nsTArray> discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return; + } + + surface = + sInstance->GetSurfaceForResetAnimation(aImageKey, aSurfaceKey, lock); + sInstance->TakeDiscard(discard, lock); + } + + // Calling Reset will acquire the AnimationSurfaceProvider::mFramesMutex + // mutex. In other places we acquire the mFramesMutex then call into the + // surface cache (acquiring the surface cache mutex), so that determines a + // lock order which we must obey by calling Reset after releasing the surface + // cache mutex. + if (surface) { + DrawableSurface drawableSurface = + surface->GetDrawableSurfaceEvenIfPlaceholder(); + if (drawableSurface) { + MOZ_ASSERT(surface->GetSurfaceKey() == aSurfaceKey, + "ResetAnimation() not returning an exact match?"); + + drawableSurface.Reset(); + } + } +} + +/* static */ +void SurfaceCache::CollectSizeOfSurfaces( + const ImageKey aImageKey, nsTArray& aCounters, + MallocSizeOf aMallocSizeOf) { + nsTArray> discard; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return; + } + + sInstance->CollectSizeOfSurfaces(aImageKey, aCounters, aMallocSizeOf, lock); + sInstance->TakeDiscard(discard, lock); + } +} + +/* static */ +size_t SurfaceCache::MaximumCapacity() { + StaticMutexAutoLock lock(sInstanceMutex); + if (!sInstance) { + return 0; + } + + return sInstance->MaximumCapacity(); +} + +/* static */ +bool SurfaceCache::IsLegalSize(const IntSize& aSize) { + // reject over-wide or over-tall images + const int32_t k64KLimit = 0x0000FFFF; + if (MOZ_UNLIKELY(aSize.width > k64KLimit || aSize.height > k64KLimit)) { + NS_WARNING("image too big"); + return false; + } + + // protect against invalid sizes + if (MOZ_UNLIKELY(aSize.height <= 0 || aSize.width <= 0)) { + return false; + } + + // check to make sure we don't overflow a 32-bit + CheckedInt32 requiredBytes = + CheckedInt32(aSize.width) * CheckedInt32(aSize.height) * 4; + if (MOZ_UNLIKELY(!requiredBytes.isValid())) { + NS_WARNING("width or height too large"); + return false; + } + return true; +} + +IntSize SurfaceCache::ClampVectorSize(const IntSize& aSize) { + // If we exceed the maximum, we need to scale the size downwards to fit. + // It shouldn't get here if it is significantly larger because + // VectorImage::UseSurfaceCacheForSize should prevent us from requesting + // a rasterized version of a surface greater than 4x the maximum. + int32_t maxSizeKB = + StaticPrefs::image_cache_max_rasterized_svg_threshold_kb(); + if (maxSizeKB <= 0) { + return aSize; + } + + int64_t proposedKB = int64_t(aSize.width) * aSize.height / 256; + if (maxSizeKB >= proposedKB) { + return aSize; + } + + double scale = sqrt(double(maxSizeKB) / proposedKB); + return IntSize(int32_t(scale * aSize.width), int32_t(scale * aSize.height)); +} + +IntSize SurfaceCache::ClampSize(ImageKey aImageKey, const IntSize& aSize) { + if (aImageKey->GetType() != imgIContainer::TYPE_VECTOR) { + return aSize; + } + + return ClampVectorSize(aSize); +} + +/* static */ +void SurfaceCache::ReleaseImageOnMainThread( + already_AddRefed aImage, bool aAlwaysProxy) { + if (NS_IsMainThread() && !aAlwaysProxy) { + RefPtr image = std::move(aImage); + return; + } + + // Don't try to dispatch the release after shutdown, we'll just leak the + // runnable. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownFinal)) { + return; + } + + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + sInstance->ReleaseImageOnMainThread(std::move(aImage), lock); + } else { + NS_ReleaseOnMainThread("SurfaceCache::ReleaseImageOnMainThread", + std::move(aImage), /* aAlwaysProxy */ true); + } +} + +/* static */ +void SurfaceCache::ClearReleasingImages() { + MOZ_ASSERT(NS_IsMainThread()); + + nsTArray> images; + { + StaticMutexAutoLock lock(sInstanceMutex); + if (sInstance) { + sInstance->TakeReleasingImages(images, lock); + } + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/SurfaceCache.h b/image/SurfaceCache.h new file mode 100644 index 0000000000..864a6ffbc4 --- /dev/null +++ b/image/SurfaceCache.h @@ -0,0 +1,509 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * SurfaceCache is a service for caching temporary surfaces and decoded image + * data in imagelib. + */ + +#ifndef mozilla_image_SurfaceCache_h +#define mozilla_image_SurfaceCache_h + +#include "mozilla/HashFunctions.h" // for HashGeneric and AddToHash +#include "mozilla/Maybe.h" // for Maybe +#include "mozilla/MemoryReporting.h" // for MallocSizeOf +#include "mozilla/NotNull.h" +#include "mozilla/SVGImageContext.h" // for SVGImageContext +#include "mozilla/gfx/2D.h" // for SourceSurface +#include "mozilla/gfx/Point.h" // for mozilla::gfx::IntSize +#include "gfx2DGlue.h" +#include "gfxPoint.h" // for gfxSize +#include "nsCOMPtr.h" // for already_AddRefed +#include "ImageRegion.h" +#include "PlaybackType.h" +#include "SurfaceFlags.h" + +namespace mozilla { +namespace image { + +class ImageResource; +class ISurfaceProvider; +class LookupResult; +class SurfaceCacheImpl; +struct SurfaceMemoryCounter; + +/* + * ImageKey contains the information we need to look up all SurfaceCache entries + * for a particular image. + */ +using ImageKey = ImageResource*; + +/* + * SurfaceKey contains the information we need to look up a specific + * SurfaceCache entry. Together with an ImageKey, this uniquely identifies the + * surface. + * + * Callers should construct a SurfaceKey using the appropriate helper function + * for their image type - either RasterSurfaceKey or VectorSurfaceKey. + */ +class SurfaceKey { + typedef gfx::IntSize IntSize; + + public: + bool operator==(const SurfaceKey& aOther) const { + return aOther.mSize == mSize && aOther.mRegion == mRegion && + aOther.mSVGContext == mSVGContext && aOther.mPlayback == mPlayback && + aOther.mFlags == mFlags; + } + + PLDHashNumber Hash() const { + PLDHashNumber hash = HashGeneric(mSize.width, mSize.height); + hash = AddToHash(hash, mRegion.map(HashIIR).valueOr(0)); + hash = AddToHash(hash, HashSIC(mSVGContext)); + hash = AddToHash(hash, uint8_t(mPlayback), uint32_t(mFlags)); + return hash; + } + + SurfaceKey CloneWithSize(const IntSize& aSize) const { + return SurfaceKey(aSize, mRegion, mSVGContext, mPlayback, mFlags); + } + + const IntSize& Size() const { return mSize; } + const Maybe& Region() const { return mRegion; } + const SVGImageContext& SVGContext() const { return mSVGContext; } + PlaybackType Playback() const { return mPlayback; } + SurfaceFlags Flags() const { return mFlags; } + + private: + SurfaceKey(const IntSize& aSize, const Maybe& aRegion, + const SVGImageContext& aSVGContext, PlaybackType aPlayback, + SurfaceFlags aFlags) + : mSize(aSize), + mRegion(aRegion), + mSVGContext(aSVGContext), + mPlayback(aPlayback), + mFlags(aFlags) {} + + static PLDHashNumber HashIIR(const ImageIntRegion& aIIR) { + return aIIR.Hash(); + } + + static PLDHashNumber HashSIC(const SVGImageContext& aSIC) { + return aSIC.Hash(); + } + + friend SurfaceKey RasterSurfaceKey(const IntSize&, SurfaceFlags, + PlaybackType); + friend SurfaceKey VectorSurfaceKey(const IntSize&, const SVGImageContext&); + friend SurfaceKey VectorSurfaceKey(const IntSize&, + const Maybe&, + const SVGImageContext&, SurfaceFlags, + PlaybackType); + + IntSize mSize; + Maybe mRegion; + SVGImageContext mSVGContext; + PlaybackType mPlayback; + SurfaceFlags mFlags; +}; + +inline SurfaceKey RasterSurfaceKey(const gfx::IntSize& aSize, + SurfaceFlags aFlags, + PlaybackType aPlayback) { + return SurfaceKey(aSize, Nothing(), SVGImageContext(), aPlayback, aFlags); +} + +inline SurfaceKey VectorSurfaceKey(const gfx::IntSize& aSize, + const Maybe& aRegion, + const SVGImageContext& aSVGContext, + SurfaceFlags aFlags, + PlaybackType aPlayback) { + return SurfaceKey(aSize, aRegion, aSVGContext, aPlayback, aFlags); +} + +inline SurfaceKey VectorSurfaceKey(const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext) { + // We don't care about aFlags for VectorImage because none of the flags we + // have right now influence VectorImage's rendering. If we add a new flag that + // *does* affect how a VectorImage renders, we'll have to change this. + // Similarly, we don't accept a PlaybackType parameter because we don't + // currently cache frames of animated SVG images. + return SurfaceKey(aSize, Nothing(), aSVGContext, PlaybackType::eStatic, + DefaultSurfaceFlags()); +} + +/** + * AvailabilityState is used to track whether an ISurfaceProvider has a surface + * available or is just a placeholder. + * + * To ensure that availability changes are atomic (and especially that internal + * SurfaceCache code doesn't have to deal with asynchronous availability + * changes), an ISurfaceProvider which starts as a placeholder can only reveal + * the fact that it now has a surface available via a call to + * SurfaceCache::SurfaceAvailable(). + * + * It also tracks whether or not there are "explicit" users of this surface + * which will not accept substitutes. This is used by SurfaceCache when pruning + * unnecessary surfaces from the cache. + */ +class AvailabilityState { + public: + static AvailabilityState StartAvailable() { return AvailabilityState(true); } + static AvailabilityState StartAsPlaceholder() { + return AvailabilityState(false); + } + + bool IsAvailable() const { return mIsAvailable; } + bool IsPlaceholder() const { return !mIsAvailable; } + bool CannotSubstitute() const { return mCannotSubstitute; } + + void SetCannotSubstitute() { mCannotSubstitute = true; } + + private: + friend class SurfaceCacheImpl; + + explicit AvailabilityState(bool aIsAvailable) + : mIsAvailable(aIsAvailable), mCannotSubstitute(false) {} + + void SetAvailable() { mIsAvailable = true; } + + bool mIsAvailable : 1; + bool mCannotSubstitute : 1; +}; + +enum class InsertOutcome : uint8_t { + SUCCESS, // Success (but see Insert documentation). + FAILURE, // Couldn't insert (e.g., for capacity reasons). + FAILURE_ALREADY_PRESENT // A surface with the same key is already present. +}; + +/** + * SurfaceCache is an ImageLib-global service that allows caching of decoded + * image surfaces, temporary surfaces (e.g. for caching rotated or clipped + * versions of images), or dynamically generated surfaces (e.g. for animations). + * SurfaceCache entries normally expire from the cache automatically if they go + * too long without being accessed. + * + * Because SurfaceCache must support both normal surfaces and dynamically + * generated surfaces, it does not actually hold surfaces directly. Instead, it + * holds ISurfaceProvider objects which can provide access to a surface when + * requested; SurfaceCache doesn't care about the details of how this is + * accomplished. + * + * Sometime it's useful to temporarily prevent entries from expiring from the + * cache. This is most often because losing the data could harm the user + * experience (for example, we often don't want to allow surfaces that are + * currently visible to expire) or because it's not possible to rematerialize + * the surface. SurfaceCache supports this through the use of image locking; see + * the comments for Insert() and LockImage() for more details. + * + * Any image which stores surfaces in the SurfaceCache *must* ensure that it + * calls RemoveImage() before it is destroyed. See the comments for + * RemoveImage() for more details. + */ +struct SurfaceCache { + typedef gfx::IntSize IntSize; + + /** + * Initialize static data. Called during imagelib module initialization. + */ + static void Initialize(); + + /** + * Release static data. Called during imagelib module shutdown. + */ + static void Shutdown(); + + /** + * Looks up the requested cache entry and returns a drawable reference to its + * associated surface. + * + * If the image associated with the cache entry is locked, then the entry will + * be locked before it is returned. + * + * If a matching ISurfaceProvider was found in the cache, but SurfaceCache + * couldn't obtain a surface from it (e.g. because it had stored its surface + * in a volatile buffer which was discarded by the OS) then it is + * automatically removed from the cache and an empty LookupResult is returned. + * Note that this will never happen to ISurfaceProviders associated with a + * locked image; SurfaceCache tells such ISurfaceProviders to keep a strong + * references to their data internally. + * + * @param aImageKey Key data identifying which image the cache entry + * belongs to. + * @param aSurfaceKey Key data which uniquely identifies the requested + * cache entry. + * @return a LookupResult which will contain a DrawableSurface + * if the cache entry was found. + */ + static LookupResult Lookup(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey, bool aMarkUsed); + + /** + * Looks up the best matching cache entry and returns a drawable reference to + * its associated surface. + * + * The result may vary from the requested cache entry only in terms of size. + * + * @param aImageKey Key data identifying which image the cache entry + * belongs to. + * @param aSurfaceKey Key data which uniquely identifies the requested + * cache entry. + * @return a LookupResult which will contain a DrawableSurface + * if a cache entry similar to the one the caller + * requested could be found. Callers can use + * LookupResult::IsExactMatch() to check whether the + * returned surface exactly matches @aSurfaceKey. + */ + static LookupResult LookupBestMatch(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey, + bool aMarkUsed); + + /** + * Insert an ISurfaceProvider into the cache. If an entry with the same + * ImageKey and SurfaceKey is already in the cache, Insert returns + * FAILURE_ALREADY_PRESENT. If a matching placeholder is already present, it + * is replaced. + * + * Cache entries will never expire as long as they remain locked, but if they + * become unlocked, they can expire either because the SurfaceCache runs out + * of capacity or because they've gone too long without being used. When it + * is first inserted, a cache entry is locked if its associated image is + * locked. When that image is later unlocked, the cache entry becomes + * unlocked too. To become locked again at that point, two things must happen: + * the image must become locked again (via LockImage()), and the cache entry + * must be touched again (via one of the Lookup() functions). + * + * All of this means that a very particular procedure has to be followed for + * cache entries which cannot be rematerialized. First, they must be inserted + * *after* the image is locked with LockImage(); if you use the other order, + * the cache entry might expire before LockImage() gets called or before the + * entry is touched again by Lookup(). Second, the image they are associated + * with must never be unlocked. + * + * If a cache entry cannot be rematerialized, it may be important to know + * whether it was inserted into the cache successfully. Insert() returns + * FAILURE if it failed to insert the cache entry, which could happen because + * of capacity reasons, or because it was already freed by the OS. If the + * cache entry isn't associated with a locked image, checking for SUCCESS or + * FAILURE is useless: the entry might expire immediately after being + * inserted, even though Insert() returned SUCCESS. Thus, many callers do not + * need to check the result of Insert() at all. + * + * @param aProvider The new cache entry to insert into the cache. + * @return SUCCESS if the cache entry was inserted successfully. (But see + * above for more information about when you should check this.) + * FAILURE if the cache entry could not be inserted, e.g. for capacity + * reasons. (But see above for more information about when you + * should check this.) + * FAILURE_ALREADY_PRESENT if an entry with the same ImageKey and + * SurfaceKey already exists in the cache. + */ + static InsertOutcome Insert(NotNull aProvider); + + /** + * Mark the cache entry @aProvider as having an available surface. This turns + * a placeholder cache entry into a normal cache entry. The cache entry + * becomes locked if the associated image is locked; otherwise, it starts in + * the unlocked state. + * + * If the cache entry containing @aProvider has already been evicted from the + * surface cache, this function has no effect. + * + * It's illegal to call this function if @aProvider is not a placeholder; by + * definition, non-placeholder ISurfaceProviders should have a surface + * available already. + * + * @param aProvider The cache entry that now has a surface available. + */ + static void SurfaceAvailable(NotNull aProvider); + + /** + * Checks if a surface of a given size could possibly be stored in the cache. + * If CanHold() returns false, Insert() will always fail to insert the + * surface, but the inverse is not true: Insert() may take more information + * into account than just image size when deciding whether to cache the + * surface, so Insert() may still fail even if CanHold() returns true. + * + * Use CanHold() to avoid the need to create a temporary surface when we know + * for sure the cache can't hold it. + * + * @param aSize The dimensions of a surface in pixels. + * @param aBytesPerPixel How many bytes each pixel of the surface requires. + * Defaults to 4, which is appropriate for RGBA or RGBX + * images. + * + * @return false if the surface cache can't hold a surface of that size. + */ + static bool CanHold(const IntSize& aSize, uint32_t aBytesPerPixel = 4); + static bool CanHold(size_t aSize); + + /** + * Locks an image. Any of the image's cache entries which are either inserted + * or accessed while the image is locked will not expire. + * + * Locking an image does not automatically lock that image's existing cache + * entries. A call to LockImage() guarantees that entries which are inserted + * afterward will not expire before the next call to UnlockImage() or + * UnlockSurfaces() for that image. Cache entries that are accessed via + * Lookup() or LookupBestMatch() after a LockImage() call will also not expire + * until the next UnlockImage() or UnlockSurfaces() call for that image. Any + * other cache entries owned by the image may expire at any time. + * + * All of an image's cache entries are removed by RemoveImage(), whether the + * image is locked or not. + * + * It's safe to call LockImage() on an image that's already locked; this has + * no effect. + * + * You must always unlock any image you lock. You may do this explicitly by + * calling UnlockImage(), or implicitly by calling RemoveImage(). Since you're + * required to call RemoveImage() when you destroy an image, this doesn't + * impose any additional requirements, but it's preferable to call + * UnlockImage() earlier if it's possible. + * + * @param aImageKey The image to lock. + */ + static void LockImage(const ImageKey aImageKey); + + /** + * Unlocks an image, allowing any of its cache entries to expire at any time. + * + * It's OK to call UnlockImage() on an image that's already unlocked; this has + * no effect. + * + * @param aImageKey The image to unlock. + */ + static void UnlockImage(const ImageKey aImageKey); + + /** + * Unlocks the existing cache entries of an image, allowing them to expire at + * any time. + * + * This does not unlock the image itself, so accessing the cache entries via + * Lookup() or LookupBestMatch() will lock them again, and prevent them from + * expiring. + * + * This is intended to be used in situations where it's no longer clear that + * all of the cache entries owned by an image are needed. Calling + * UnlockSurfaces() and then taking some action that will cause Lookup() to + * touch any cache entries that are still useful will permit the remaining + * entries to expire from the cache. + * + * If the image is unlocked, this has no effect. + * + * @param aImageKey The image which should have its existing cache entries + * unlocked. + */ + static void UnlockEntries(const ImageKey aImageKey); + + /** + * Removes all cache entries (including placeholders) associated with the + * given image from the cache. If the image is locked, it is automatically + * unlocked. + * + * This MUST be called, at a minimum, when an Image which could be storing + * entries in the surface cache is destroyed. If another image were allocated + * at the same address it could result in subtle, difficult-to-reproduce bugs. + * + * @param aImageKey The image which should be removed from the cache. + */ + static void RemoveImage(const ImageKey aImageKey); + + /** + * Attempts to remove cache entries (including placeholders) associated with + * the given image from the cache, assuming there is an equivalent entry that + * it is able substitute that entry with. Note that this only applies if the + * image is in factor of 2 mode. If it is not, this operation does nothing. + * + * @param aImageKey The image whose cache which should be pruned. + */ + static void PruneImage(const ImageKey aImageKey); + + /** + * Removes all rasterized cache entries (including placeholders) associated + * with the given image from the cache. Any blob recordings are marked as + * dirty and must be regenerated. + * + * @param aImageKey The image whose cache which should be regenerated. + * + * @returns true if any recordings were invalidated, else false. + */ + static bool InvalidateImage(const ImageKey aImageKey); + + /** + * Evicts all evictable entries from the cache. + * + * All entries are evictable except for entries associated with locked images. + * Non-evictable entries can only be removed by RemoveImage(). + */ + static void DiscardAll(); + + /** + * Calls Reset on the ISurfaceProvider (which is currently only implemented + * for AnimationSurfaceProvider). This is needed because we need to call Reset + * on AnimationSurfaceProvider while they are in placeholder status and there + * is no way to access a surface cache entry from outside of the surface cache + * when it's in placeholder status. + */ + static void ResetAnimation(const ImageKey aImageKey, + const SurfaceKey& aSurfaceKey); + + /** + * Collects an accounting of the surfaces contained in the SurfaceCache for + * the given image, along with their size and various other metadata. + * + * This is intended for use with memory reporting. + * + * @param aImageKey The image to report memory usage for. + * @param aCounters An array into which the report for each surface will + * be written. + * @param aMallocSizeOf A fallback malloc memory reporting function. + */ + static void CollectSizeOfSurfaces(const ImageKey aImageKey, + nsTArray& aCounters, + MallocSizeOf aMallocSizeOf); + + /** + * @return maximum capacity of the SurfaceCache in bytes. This is only exposed + * for use by tests; normal code should use CanHold() instead. + */ + static size_t MaximumCapacity(); + + /** + * @return true if the given size is valid. + */ + static bool IsLegalSize(const IntSize& aSize); + + /** + * @return clamped size for the given vector image size to rasterize at. + */ + static IntSize ClampVectorSize(const IntSize& aSize); + + /** + * @return clamped size for the given image and size to rasterize at. + */ + static IntSize ClampSize(const ImageKey aImageKey, const IntSize& aSize); + + /** + * Release image on main thread. + * The function uses SurfaceCache to release pending releasing images quickly. + */ + static void ReleaseImageOnMainThread(already_AddRefed aImage, + bool aAlwaysProxy = false); + + /** + * Clear all pending releasing images. + */ + static void ClearReleasingImages(); + + private: + virtual ~SurfaceCache() = 0; // Forbid instantiation. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfaceCache_h diff --git a/image/SurfaceCacheUtils.cpp b/image/SurfaceCacheUtils.cpp new file mode 100644 index 0000000000..e92197ca7c --- /dev/null +++ b/image/SurfaceCacheUtils.cpp @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SurfaceCacheUtils.h" + +#include "SurfaceCache.h" + +namespace mozilla { +namespace image { + +/* static */ +void SurfaceCacheUtils::DiscardAll() { SurfaceCache::DiscardAll(); } + +} // namespace image +} // namespace mozilla diff --git a/image/SurfaceCacheUtils.h b/image/SurfaceCacheUtils.h new file mode 100644 index 0000000000..0e1e2431f9 --- /dev/null +++ b/image/SurfaceCacheUtils.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_SurfaceCacheUtils_h +#define mozilla_image_SurfaceCacheUtils_h + +/** + * SurfaceCacheUtils provides an ImageLib-external API to interact with + * ImageLib's SurfaceCache. + */ + +namespace mozilla { +namespace image { + +class SurfaceCacheUtils { + public: + /** + * Evicts all evictable entries from the surface cache. + * + * See the documentation for SurfaceCache::DiscardAll() for the details. + */ + static void DiscardAll(); + + private: + virtual ~SurfaceCacheUtils() = 0; // Forbid instantiation. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfaceCacheUtils_h diff --git a/image/SurfaceFilters.h b/image/SurfaceFilters.h new file mode 100644 index 0000000000..92c406386a --- /dev/null +++ b/image/SurfaceFilters.h @@ -0,0 +1,1454 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This header contains various SurfaceFilter implementations that apply + * transformations to image data, for usage with SurfacePipe. + */ + +#ifndef mozilla_image_SurfaceFilters_h +#define mozilla_image_SurfaceFilters_h + +#include +#include +#include + +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Swizzle.h" +#include "skia/src/core/SkBlitRow.h" + +#include "DownscalingFilter.h" +#include "SurfaceCache.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////////////// +// SwizzleFilter +////////////////////////////////////////////////////////////////////////////// + +template +class SwizzleFilter; + +/** + * A configuration struct for SwizzleFilter. + */ +struct SwizzleConfig { + template + using Filter = SwizzleFilter; + gfx::SurfaceFormat mInFormat; + gfx::SurfaceFormat mOutFormat; + bool mPremultiplyAlpha; +}; + +/** + * SwizzleFilter performs premultiplication, swizzling and unpacking on + * rows written to it. It can use accelerated methods to perform these + * operations if supported on the platform. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class SwizzleFilter final : public SurfaceFilter { + public: + SwizzleFilter() : mSwizzleFn(nullptr) {} + + template + nsresult Configure(const SwizzleConfig& aConfig, const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + if (aConfig.mPremultiplyAlpha) { + mSwizzleFn = gfx::PremultiplyRow(aConfig.mInFormat, aConfig.mOutFormat); + } else { + mSwizzleFn = gfx::SwizzleRow(aConfig.mInFormat, aConfig.mOutFormat); + } + + if (!mSwizzleFn) { + return NS_ERROR_INVALID_ARG; + } + + ConfigureFilter(mNext.InputSize(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { return mNext.ResetToFirstRow(); } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + uint8_t* rowPtr = mNext.CurrentRowPointer(); + if (!rowPtr) { + return nullptr; // We already got all the input rows we expect. + } + + mSwizzleFn(aInputRow, rowPtr, mNext.InputSize().width); + return mNext.AdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + return DoAdvanceRowFromBuffer(mNext.CurrentRowPointer()); + } + + Next mNext; /// The next SurfaceFilter in the chain. + + gfx::SwizzleRowFn mSwizzleFn; +}; + +////////////////////////////////////////////////////////////////////////////// +// ColorManagementFilter +////////////////////////////////////////////////////////////////////////////// + +template +class ColorManagementFilter; + +/** + * A configuration struct for ColorManagementFilter. + */ +struct ColorManagementConfig { + template + using Filter = ColorManagementFilter; + qcms_transform* mTransform; +}; + +/** + * ColorManagementFilter performs color transforms with qcms on rows written + * to it. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class ColorManagementFilter final : public SurfaceFilter { + public: + ColorManagementFilter() : mTransform(nullptr) {} + + template + nsresult Configure(const ColorManagementConfig& aConfig, + const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + if (!aConfig.mTransform) { + return NS_ERROR_INVALID_ARG; + } + + mTransform = aConfig.mTransform; + ConfigureFilter(mNext.InputSize(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { return mNext.ResetToFirstRow(); } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + qcms_transform_data(mTransform, aInputRow, mNext.CurrentRowPointer(), + mNext.InputSize().width); + return mNext.AdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + return DoAdvanceRowFromBuffer(mNext.CurrentRowPointer()); + } + + Next mNext; /// The next SurfaceFilter in the chain. + + qcms_transform* mTransform; +}; + +////////////////////////////////////////////////////////////////////////////// +// DeinterlacingFilter +////////////////////////////////////////////////////////////////////////////// + +template +class DeinterlacingFilter; + +/** + * A configuration struct for DeinterlacingFilter. + * + * The 'PixelType' template parameter should be either uint32_t (for output to a + * SurfaceSink) or uint8_t (for output to a PalettedSurfaceSink). + */ +template +struct DeinterlacingConfig { + template + using Filter = DeinterlacingFilter; + bool mProgressiveDisplay; /// If true, duplicate rows during deinterlacing + /// to make progressive display look better, at + /// the cost of some performance. +}; + +/** + * DeinterlacingFilter performs deinterlacing by reordering the rows that are + * written to it. + * + * The 'PixelType' template parameter should be either uint32_t (for output to a + * SurfaceSink) or uint8_t (for output to a PalettedSurfaceSink). + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class DeinterlacingFilter final : public SurfaceFilter { + public: + DeinterlacingFilter() + : mInputRow(0), mOutputRow(0), mPass(0), mProgressiveDisplay(true) {} + + template + nsresult Configure(const DeinterlacingConfig& aConfig, + const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + gfx::IntSize outputSize = mNext.InputSize(); + mProgressiveDisplay = aConfig.mProgressiveDisplay; + + const CheckedUint32 bufferSize = CheckedUint32(outputSize.width) * + CheckedUint32(outputSize.height) * + CheckedUint32(sizeof(PixelType)); + + // Use the size of the SurfaceCache as a heuristic to avoid gigantic + // allocations. Even if DownscalingFilter allowed us to allocate space for + // the output image, the deinterlacing buffer may still be too big, and + // fallible allocation won't always save us in the presence of overcommit. + if (!bufferSize.isValid() || !SurfaceCache::CanHold(bufferSize.value())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Allocate the buffer, which contains deinterlaced scanlines of the image. + // The buffer is necessary so that we can output rows which have already + // been deinterlaced again on subsequent passes. Since a later stage in the + // pipeline may be transforming the rows it receives (for example, by + // downscaling them), the rows may no longer exist in their original form on + // the surface itself. + mBuffer.reset(new (fallible) uint8_t[bufferSize.value()]); + if (MOZ_UNLIKELY(!mBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Clear the buffer to avoid writing uninitialized memory to the output. + memset(mBuffer.get(), 0, bufferSize.value()); + + ConfigureFilter(outputSize, sizeof(PixelType)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { + mNext.ResetToFirstRow(); + mPass = 0; + mInputRow = 0; + mOutputRow = InterlaceOffset(mPass); + return GetRowPointer(mOutputRow); + } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + CopyInputRow(aInputRow); + return DoAdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + if (mPass >= 4) { + return nullptr; // We already finished all passes. + } + if (mInputRow >= InputSize().height) { + return nullptr; // We already got all the input rows we expect. + } + + // Duplicate from the first Haeberli row to the remaining Haeberli rows + // within the buffer. + DuplicateRows( + HaeberliOutputStartRow(mPass, mProgressiveDisplay, mOutputRow), + HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + mOutputRow)); + + // Write the current set of Haeberli rows (which contains the current row) + // to the next stage in the pipeline. + OutputRows(HaeberliOutputStartRow(mPass, mProgressiveDisplay, mOutputRow), + HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + mOutputRow)); + + // Determine which output row the next input row corresponds to. + bool advancedPass = false; + uint32_t stride = InterlaceStride(mPass); + int32_t nextOutputRow = mOutputRow + stride; + while (nextOutputRow >= InputSize().height) { + // Copy any remaining rows from the buffer. + if (!advancedPass) { + OutputRows(HaeberliOutputUntilRow(mPass, mProgressiveDisplay, + InputSize(), mOutputRow), + InputSize().height); + } + + // We finished the current pass; advance to the next one. + mPass++; + if (mPass >= 4) { + return nullptr; // Finished all passes. + } + + // Tell the next pipeline stage that we're starting the next pass. + mNext.ResetToFirstRow(); + + // Update our state to reflect the pass change. + advancedPass = true; + stride = InterlaceStride(mPass); + nextOutputRow = InterlaceOffset(mPass); + } + + MOZ_ASSERT(nextOutputRow >= 0); + MOZ_ASSERT(nextOutputRow < InputSize().height); + + MOZ_ASSERT( + HaeberliOutputStartRow(mPass, mProgressiveDisplay, nextOutputRow) >= 0); + MOZ_ASSERT(HaeberliOutputStartRow(mPass, mProgressiveDisplay, + nextOutputRow) < InputSize().height); + MOZ_ASSERT(HaeberliOutputStartRow(mPass, mProgressiveDisplay, + nextOutputRow) <= nextOutputRow); + + MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + nextOutputRow) >= 0); + MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + nextOutputRow) <= InputSize().height); + MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + nextOutputRow) > nextOutputRow); + + int32_t nextHaeberliOutputRow = + HaeberliOutputStartRow(mPass, mProgressiveDisplay, nextOutputRow); + + // Copy rows from the buffer until we reach the desired output row. + if (advancedPass) { + OutputRows(0, nextHaeberliOutputRow); + } else { + OutputRows(HaeberliOutputUntilRow(mPass, mProgressiveDisplay, InputSize(), + mOutputRow), + nextHaeberliOutputRow); + } + + // Update our position within the buffer. + mInputRow++; + mOutputRow = nextOutputRow; + + // We'll actually write to the first Haeberli output row, then copy it until + // we reach the last Haeberli output row. The assertions above make sure + // this always includes mOutputRow. + return GetRowPointer(nextHaeberliOutputRow); + } + + private: + static uint32_t InterlaceOffset(uint32_t aPass) { + MOZ_ASSERT(aPass < 4, "Invalid pass"); + static const uint8_t offset[] = {0, 4, 2, 1}; + return offset[aPass]; + } + + static uint32_t InterlaceStride(uint32_t aPass) { + MOZ_ASSERT(aPass < 4, "Invalid pass"); + static const uint8_t stride[] = {8, 8, 4, 2}; + return stride[aPass]; + } + + static int32_t HaeberliOutputStartRow(uint32_t aPass, + bool aProgressiveDisplay, + int32_t aOutputRow) { + MOZ_ASSERT(aPass < 4, "Invalid pass"); + static const uint8_t firstRowOffset[] = {3, 1, 0, 0}; + + if (aProgressiveDisplay) { + return std::max(aOutputRow - firstRowOffset[aPass], 0); + } else { + return aOutputRow; + } + } + + static int32_t HaeberliOutputUntilRow(uint32_t aPass, + bool aProgressiveDisplay, + const gfx::IntSize& aInputSize, + int32_t aOutputRow) { + MOZ_ASSERT(aPass < 4, "Invalid pass"); + static const uint8_t lastRowOffset[] = {4, 2, 1, 0}; + + if (aProgressiveDisplay) { + return std::min(aOutputRow + lastRowOffset[aPass], + aInputSize.height - 1) + + 1; // Add one because this is an open interval on the right. + } else { + return aOutputRow + 1; + } + } + + void DuplicateRows(int32_t aStart, int32_t aUntil) { + MOZ_ASSERT(aStart >= 0); + MOZ_ASSERT(aUntil >= 0); + + if (aUntil <= aStart || aStart >= InputSize().height) { + return; + } + + // The source row is the first row in the range. + const uint8_t* sourceRowPointer = GetRowPointer(aStart); + + // We duplicate the source row into each subsequent row in the range. + for (int32_t destRow = aStart + 1; destRow < aUntil; ++destRow) { + uint8_t* destRowPointer = GetRowPointer(destRow); + memcpy(destRowPointer, sourceRowPointer, + InputSize().width * sizeof(PixelType)); + } + } + + void OutputRows(int32_t aStart, int32_t aUntil) { + MOZ_ASSERT(aStart >= 0); + MOZ_ASSERT(aUntil >= 0); + + if (aUntil <= aStart || aStart >= InputSize().height) { + return; + } + + for (int32_t rowToOutput = aStart; rowToOutput < aUntil; ++rowToOutput) { + mNext.WriteBuffer( + reinterpret_cast(GetRowPointer(rowToOutput))); + } + } + + uint8_t* GetRowPointer(uint32_t aRow) const { +#ifdef DEBUG + uint64_t offset64 = uint64_t(aRow) * uint64_t(InputSize().width) * + uint64_t(sizeof(PixelType)); + uint64_t bufferLength = uint64_t(InputSize().width) * + uint64_t(InputSize().height) * + uint64_t(sizeof(PixelType)); + MOZ_ASSERT(offset64 < bufferLength, "Start of row is outside of image"); + MOZ_ASSERT( + offset64 + uint64_t(InputSize().width) * uint64_t(sizeof(PixelType)) <= + bufferLength, + "End of row is outside of image"); +#endif + uint32_t offset = aRow * InputSize().width * sizeof(PixelType); + return mBuffer.get() + offset; + } + + Next mNext; /// The next SurfaceFilter in the chain. + + UniquePtr mBuffer; /// The buffer used to store reordered rows. + int32_t mInputRow; /// The current row we're reading. (0-indexed) + int32_t mOutputRow; /// The current row we're writing. (0-indexed) + uint8_t mPass; /// Which pass we're on. (0-indexed) + bool mProgressiveDisplay; /// If true, duplicate rows to optimize for + /// progressive display. +}; + +////////////////////////////////////////////////////////////////////////////// +// BlendAnimationFilter +////////////////////////////////////////////////////////////////////////////// + +template +class BlendAnimationFilter; + +/** + * A configuration struct for BlendAnimationFilter. + */ +struct BlendAnimationConfig { + template + using Filter = BlendAnimationFilter; + Decoder* mDecoder; /// The decoder producing the animation. +}; + +/** + * BlendAnimationFilter turns a partial image as part of an animation into a + * complete frame given its frame rect, blend method, and the base frame's + * data buffer, frame rect and disposal method. Any excess data caused by a + * frame rect not being contained by the output size will be discarded. + * + * The base frame is an already produced complete frame from the animation. + * It may be any previous frame depending on the disposal method, although + * most often it will be the immediate previous frame to the current we are + * generating. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class BlendAnimationFilter final : public SurfaceFilter { + public: + BlendAnimationFilter() + : mRow(0), + mRowLength(0), + mRecycleRow(0), + mRecycleRowMost(0), + mRecycleRowOffset(0), + mRecycleRowLength(0), + mClearRow(0), + mClearRowMost(0), + mClearPrefixLength(0), + mClearInfixOffset(0), + mClearInfixLength(0), + mClearPostfixOffset(0), + mClearPostfixLength(0), + mOverProc(nullptr), + mBaseFrameStartPtr(nullptr), + mBaseFrameRowPtr(nullptr) {} + + template + nsresult Configure(const BlendAnimationConfig& aConfig, + const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + imgFrame* currentFrame = aConfig.mDecoder->GetCurrentFrame(); + if (!currentFrame) { + MOZ_ASSERT_UNREACHABLE("Decoder must have current frame!"); + return NS_ERROR_FAILURE; + } + + mFrameRect = mUnclampedFrameRect = currentFrame->GetBlendRect(); + gfx::IntSize outputSize = mNext.InputSize(); + mRowLength = outputSize.width * sizeof(uint32_t); + + // Forbid frame rects with negative size. + if (mUnclampedFrameRect.width < 0 || mUnclampedFrameRect.height < 0) { + return NS_ERROR_FAILURE; + } + + // Clamp mFrameRect to the output size. + gfx::IntRect outputRect(0, 0, outputSize.width, outputSize.height); + mFrameRect = mFrameRect.Intersect(outputRect); + bool fullFrame = outputRect.IsEqualEdges(mFrameRect); + + // If there's no intersection, |mFrameRect| will be an empty rect positioned + // at the maximum of |inputRect|'s and |aFrameRect|'s coordinates, which is + // not what we want. Force it to (0, 0) sized 0 x 0 in that case. + if (mFrameRect.IsEmpty()) { + mFrameRect.SetRect(0, 0, 0, 0); + } + + BlendMethod blendMethod = currentFrame->GetBlendMethod(); + switch (blendMethod) { + default: + blendMethod = BlendMethod::SOURCE; + MOZ_FALLTHROUGH_ASSERT("Unexpected blend method!"); + case BlendMethod::SOURCE: + // Default, overwrites base frame data (if any) with new. + break; + case BlendMethod::OVER: + // OVER only has an impact on the output if we have new data to blend + // with. + if (mFrameRect.IsEmpty()) { + blendMethod = BlendMethod::SOURCE; + } + break; + } + + // Determine what we need to clear and what we need to copy. If this frame + // is a full frame and uses source blending, there is no need to consider + // the disposal method of the previous frame. + gfx::IntRect dirtyRect(outputRect); + gfx::IntRect clearRect; + if (!fullFrame || blendMethod != BlendMethod::SOURCE) { + const RawAccessFrameRef& restoreFrame = + aConfig.mDecoder->GetRestoreFrameRef(); + if (restoreFrame) { + MOZ_ASSERT(restoreFrame->GetSize() == outputSize); + MOZ_ASSERT(restoreFrame->IsFinished()); + + // We can safely use this pointer without holding a RawAccessFrameRef + // because the decoder will keep it alive for us. + mBaseFrameStartPtr = restoreFrame.Data(); + MOZ_ASSERT(mBaseFrameStartPtr); + + gfx::IntRect restoreBlendRect = restoreFrame->GetBoundedBlendRect(); + gfx::IntRect restoreDirtyRect = aConfig.mDecoder->GetRestoreDirtyRect(); + switch (restoreFrame->GetDisposalMethod()) { + default: + case DisposalMethod::RESTORE_PREVIOUS: + MOZ_FALLTHROUGH_ASSERT("Unexpected DisposalMethod"); + case DisposalMethod::NOT_SPECIFIED: + case DisposalMethod::KEEP: + dirtyRect = mFrameRect.Union(restoreDirtyRect); + break; + case DisposalMethod::CLEAR: + // We only need to clear if the rect is outside the frame rect (i.e. + // overwrites a non-overlapping area) or the blend method may cause + // us to combine old data and new. + if (!mFrameRect.Contains(restoreBlendRect) || + blendMethod == BlendMethod::OVER) { + clearRect = restoreBlendRect; + } + + // If we are clearing the whole frame, we do not need to retain a + // reference to the base frame buffer. + if (outputRect.IsEqualEdges(clearRect)) { + mBaseFrameStartPtr = nullptr; + } else { + dirtyRect = mFrameRect.Union(restoreDirtyRect).Union(clearRect); + } + break; + } + } else if (!fullFrame) { + // This must be the first frame, clear everything. + clearRect = outputRect; + } + } + + // We may be able to reuse parts of our underlying buffer that we are + // writing the new frame to. The recycle rect gives us the invalidation + // region which needs to be copied from the restore frame. + const gfx::IntRect& recycleRect = aConfig.mDecoder->GetRecycleRect(); + mRecycleRow = recycleRect.y; + mRecycleRowMost = recycleRect.YMost(); + mRecycleRowOffset = recycleRect.x * sizeof(uint32_t); + mRecycleRowLength = recycleRect.width * sizeof(uint32_t); + + if (!clearRect.IsEmpty()) { + // The clear rect interacts with the recycle rect because we need to copy + // the prefix and postfix data from the base frame. The one thing we do + // know is that the infix area is always cleared explicitly. + mClearRow = clearRect.y; + mClearRowMost = clearRect.YMost(); + mClearInfixOffset = clearRect.x * sizeof(uint32_t); + mClearInfixLength = clearRect.width * sizeof(uint32_t); + + // The recycle row offset is where we need to begin copying base frame + // data for a row. If this offset begins after or at the clear infix + // offset, then there is no prefix data at all. + if (mClearInfixOffset > mRecycleRowOffset) { + mClearPrefixLength = mClearInfixOffset - mRecycleRowOffset; + } + + // Similar to the prefix, if the postfix offset begins outside the recycle + // rect, then we know we already have all the data we need. + mClearPostfixOffset = mClearInfixOffset + mClearInfixLength; + size_t recycleRowEndOffset = mRecycleRowOffset + mRecycleRowLength; + if (mClearPostfixOffset < recycleRowEndOffset) { + mClearPostfixLength = recycleRowEndOffset - mClearPostfixOffset; + } + } + + // The dirty rect, or delta between the current frame and the previous frame + // (chronologically, not necessarily the restore frame) is the last + // animation parameter we need to initialize the new frame with. + currentFrame->SetDirtyRect(dirtyRect); + + if (!mBaseFrameStartPtr) { + // Switch to SOURCE if no base frame to ensure we don't allocate an + // intermediate buffer below. OVER does nothing without the base frame + // data. + blendMethod = BlendMethod::SOURCE; + } + + // Skia provides arch-specific accelerated methods to perform blending. + // Note that this is an internal Skia API and may be prone to change, + // but we avoid the overhead of setting up Skia objects. + if (blendMethod == BlendMethod::OVER) { + mOverProc = SkBlitRow::Factory32(SkBlitRow::kSrcPixelAlpha_Flag32); + MOZ_ASSERT(mOverProc); + } + + // We don't need an intermediate buffer unless the unclamped frame rect + // width is larger than the clamped frame rect width. In that case, the + // caller will end up writing data that won't end up in the final image at + // all, and we'll need a buffer to give that data a place to go. + if (mFrameRect.width < mUnclampedFrameRect.width || mOverProc) { + mBuffer.reset(new (fallible) + uint8_t[mUnclampedFrameRect.width * sizeof(uint32_t)]); + if (MOZ_UNLIKELY(!mBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memset(mBuffer.get(), 0, mUnclampedFrameRect.width * sizeof(uint32_t)); + } + + ConfigureFilter(mUnclampedFrameRect.Size(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { + uint8_t* rowPtr = mNext.ResetToFirstRow(); + if (rowPtr == nullptr) { + mRow = mFrameRect.YMost(); + return nullptr; + } + + mRow = 0; + mBaseFrameRowPtr = mBaseFrameStartPtr; + + while (mRow < mFrameRect.y) { + WriteBaseFrameRow(); + AdvanceRowOutsideFrameRect(); + } + + // We're at the beginning of the frame rect now, so return if we're either + // ready for input or we're already done. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + if (!mFrameRect.IsEmpty() || rowPtr == nullptr) { + // Note that the pointer we're returning is for the next row we're + // actually going to write to, but we may discard writes before that point + // if mRow < mFrameRect.y. + mRow = mUnclampedFrameRect.y; + WriteBaseFrameRow(); + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect, but the frame rect + // is empty, so we need to output the rest of the image immediately. Advance + // to the end of the next pipeline stage's buffer, outputting rows that are + // copied from the base frame and/or cleared. + WriteBaseFrameRowsUntilComplete(); + + mRow = mFrameRect.YMost(); + return nullptr; // We're done. + } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + CopyInputRow(aInputRow); + return DoAdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + uint8_t* rowPtr = nullptr; + + const int32_t currentRow = mRow; + mRow++; + + // The unclamped frame rect has a negative offset which means -y rows from + // the decoder need to be discarded before we advance properly. + if (currentRow >= 0 && mBaseFrameRowPtr) { + mBaseFrameRowPtr += mRowLength; + } + + if (currentRow < mFrameRect.y) { + // This row is outside of the frame rect, so just drop it on the floor. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + return AdjustRowPointer(rowPtr); + } else if (NS_WARN_IF(currentRow >= mFrameRect.YMost())) { + return nullptr; + } + + // If we had to buffer, merge the data into the row. Otherwise we had the + // decoder write directly to the next stage's buffer. + if (mBuffer) { + int32_t width = mFrameRect.width; + uint32_t* dst = reinterpret_cast(mNext.CurrentRowPointer()); + uint32_t* src = reinterpret_cast(mBuffer.get()) - + std::min(mUnclampedFrameRect.x, 0); + dst += mFrameRect.x; + if (mOverProc) { + mOverProc(dst, src, width, 0xFF); + } else { + memcpy(dst, src, width * sizeof(uint32_t)); + } + rowPtr = mNext.AdvanceRow() ? mBuffer.get() : nullptr; + } else { + MOZ_ASSERT(!mOverProc); + rowPtr = mNext.AdvanceRow(); + } + + // If there's still more data coming or we're already done, just adjust the + // pointer and return. + if (mRow < mFrameRect.YMost() || rowPtr == nullptr) { + WriteBaseFrameRow(); + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect. Advance to the end + // of the next pipeline stage's buffer, outputting rows that are copied from + // the base frame and/or cleared. + WriteBaseFrameRowsUntilComplete(); + + return nullptr; // We're done. + } + + private: + void WriteBaseFrameRowsUntilComplete() { + do { + WriteBaseFrameRow(); + } while (AdvanceRowOutsideFrameRect()); + } + + void WriteBaseFrameRow() { + uint8_t* dest = mNext.CurrentRowPointer(); + if (!dest) { + return; + } + + // No need to copy pixels from the base frame for rows that will not change + // between the recycled frame and the new frame. + bool needBaseFrame = mRow >= mRecycleRow && mRow < mRecycleRowMost; + + if (!mBaseFrameRowPtr) { + // No base frame, so we are clearing everything. + if (needBaseFrame) { + memset(dest + mRecycleRowOffset, 0, mRecycleRowLength); + } + } else if (mClearRow <= mRow && mClearRowMost > mRow) { + // We have a base frame, but we are inside the area to be cleared. + // Only copy the data we need from the source. + if (needBaseFrame) { + memcpy(dest + mRecycleRowOffset, mBaseFrameRowPtr + mRecycleRowOffset, + mClearPrefixLength); + memcpy(dest + mClearPostfixOffset, + mBaseFrameRowPtr + mClearPostfixOffset, mClearPostfixLength); + } + memset(dest + mClearInfixOffset, 0, mClearInfixLength); + } else if (needBaseFrame) { + memcpy(dest + mRecycleRowOffset, mBaseFrameRowPtr + mRecycleRowOffset, + mRecycleRowLength); + } + } + + bool AdvanceRowOutsideFrameRect() { + // The unclamped frame rect may have a negative offset however we should + // never be advancing the row via this path (otherwise mBaseFrameRowPtr + // will be wrong. + MOZ_ASSERT(mRow >= 0); + MOZ_ASSERT(mRow < mFrameRect.y || mRow >= mFrameRect.YMost()); + + mRow++; + if (mBaseFrameRowPtr) { + mBaseFrameRowPtr += mRowLength; + } + + return mNext.AdvanceRow() != nullptr; + } + + uint8_t* AdjustRowPointer(uint8_t* aNextRowPointer) const { + if (mBuffer) { + MOZ_ASSERT(aNextRowPointer == mBuffer.get() || + aNextRowPointer == nullptr); + return aNextRowPointer; // No adjustment needed for an intermediate + // buffer. + } + + if (mFrameRect.IsEmpty() || mRow >= mFrameRect.YMost() || + aNextRowPointer == nullptr) { + return nullptr; // Nothing left to write. + } + + MOZ_ASSERT(!mOverProc); + return aNextRowPointer + mFrameRect.x * sizeof(uint32_t); + } + + Next mNext; /// The next SurfaceFilter in the chain. + + gfx::IntRect mFrameRect; /// The surface subrect which contains data, + /// clamped to the image size. + gfx::IntRect mUnclampedFrameRect; /// The frame rect before clamping. + UniquePtr mBuffer; /// The intermediate buffer, if one is + /// necessary because the frame rect width + /// is larger than the image's logical width. + int32_t mRow; /// The row in unclamped frame rect space + /// that we're currently writing. + size_t mRowLength; /// Length in bytes of a row that is the input + /// for the next filter. + int32_t mRecycleRow; /// The starting row of the recycle rect. + int32_t mRecycleRowMost; /// The ending row of the recycle rect. + size_t mRecycleRowOffset; /// Row offset in bytes of the recycle rect. + size_t mRecycleRowLength; /// Row length in bytes of the recycle rect. + + /// The frame area to clear before blending the current frame. + int32_t mClearRow; /// The starting row of the clear rect. + int32_t mClearRowMost; /// The ending row of the clear rect. + size_t mClearPrefixLength; /// Row length in bytes of clear prefix. + size_t mClearInfixOffset; /// Row offset in bytes of clear area. + size_t mClearInfixLength; /// Row length in bytes of clear area. + size_t mClearPostfixOffset; /// Row offset in bytes of clear postfix. + size_t mClearPostfixLength; /// Row length in bytes of clear postfix. + + SkBlitRow::Proc32 mOverProc; /// Function pointer to perform over blending. + const uint8_t* + mBaseFrameStartPtr; /// Starting row pointer to the base frame + /// data from which we copy pixel data from. + const uint8_t* mBaseFrameRowPtr; /// Current row pointer to the base frame + /// data. +}; + +////////////////////////////////////////////////////////////////////////////// +// RemoveFrameRectFilter +////////////////////////////////////////////////////////////////////////////// + +template +class RemoveFrameRectFilter; + +/** + * A configuration struct for RemoveFrameRectFilter. + */ +struct RemoveFrameRectConfig { + template + using Filter = RemoveFrameRectFilter; + gfx::IntRect mFrameRect; /// The surface subrect which contains data. +}; + +/** + * RemoveFrameRectFilter turns an image with a frame rect that does not match + * its logical size into an image with no frame rect. It does this by writing + * transparent pixels into any padding regions and throwing away excess data. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class RemoveFrameRectFilter final : public SurfaceFilter { + public: + RemoveFrameRectFilter() : mRow(0) {} + + template + nsresult Configure(const RemoveFrameRectConfig& aConfig, + const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + mFrameRect = mUnclampedFrameRect = aConfig.mFrameRect; + gfx::IntSize outputSize = mNext.InputSize(); + + // Forbid frame rects with negative size. + if (aConfig.mFrameRect.Width() < 0 || aConfig.mFrameRect.Height() < 0) { + return NS_ERROR_INVALID_ARG; + } + + // Clamp mFrameRect to the output size. + gfx::IntRect outputRect(0, 0, outputSize.width, outputSize.height); + mFrameRect = mFrameRect.Intersect(outputRect); + + // If there's no intersection, |mFrameRect| will be an empty rect positioned + // at the maximum of |inputRect|'s and |aFrameRect|'s coordinates, which is + // not what we want. Force it to (0, 0) in that case. + if (mFrameRect.IsEmpty()) { + mFrameRect.MoveTo(0, 0); + } + + // We don't need an intermediate buffer unless the unclamped frame rect + // width is larger than the clamped frame rect width. In that case, the + // caller will end up writing data that won't end up in the final image at + // all, and we'll need a buffer to give that data a place to go. + if (mFrameRect.Width() < mUnclampedFrameRect.Width()) { + mBuffer.reset(new ( + fallible) uint8_t[mUnclampedFrameRect.Width() * sizeof(uint32_t)]); + if (MOZ_UNLIKELY(!mBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memset(mBuffer.get(), 0, mUnclampedFrameRect.Width() * sizeof(uint32_t)); + } + + ConfigureFilter(mUnclampedFrameRect.Size(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { + uint8_t* rowPtr = mNext.ResetToFirstRow(); + if (rowPtr == nullptr) { + mRow = mFrameRect.YMost(); + return nullptr; + } + + mRow = mUnclampedFrameRect.Y(); + + // Advance the next pipeline stage to the beginning of the frame rect, + // outputting blank rows. + if (mFrameRect.Y() > 0) { + for (int32_t rowToOutput = 0; rowToOutput < mFrameRect.Y(); + ++rowToOutput) { + mNext.WriteEmptyRow(); + } + } + + // We're at the beginning of the frame rect now, so return if we're either + // ready for input or we're already done. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + if (!mFrameRect.IsEmpty() || rowPtr == nullptr) { + // Note that the pointer we're returning is for the next row we're + // actually going to write to, but we may discard writes before that point + // if mRow < mFrameRect.y. + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect, but the frame rect + // is empty, so we need to output the rest of the image immediately. Advance + // to the end of the next pipeline stage's buffer, outputting blank rows. + while (mNext.WriteEmptyRow() == WriteState::NEED_MORE_DATA) { + } + + mRow = mFrameRect.YMost(); + return nullptr; // We're done. + } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + CopyInputRow(aInputRow); + return DoAdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + uint8_t* rowPtr = nullptr; + + const int32_t currentRow = mRow; + mRow++; + + if (currentRow < mFrameRect.Y()) { + // This row is outside of the frame rect, so just drop it on the floor. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + return AdjustRowPointer(rowPtr); + } else if (currentRow >= mFrameRect.YMost()) { + NS_WARNING("RemoveFrameRectFilter: Advancing past end of frame rect"); + return nullptr; + } + + // If we had to buffer, copy the data. Otherwise, just advance the row. + if (mBuffer) { + // We write from the beginning of the buffer unless + // |mUnclampedFrameRect.x| is negative; if that's the case, we have to + // skip the portion of the unclamped frame rect that's outside the row. + uint32_t* source = reinterpret_cast(mBuffer.get()) - + std::min(mUnclampedFrameRect.X(), 0); + + // We write |mFrameRect.width| columns starting at |mFrameRect.x|; we've + // already clamped these values to the size of the output, so we don't + // have to worry about bounds checking here (though WriteBuffer() will do + // it for us in any case). + WriteState state = + mNext.WriteBuffer(source, mFrameRect.X(), mFrameRect.Width()); + + rowPtr = state == WriteState::NEED_MORE_DATA ? mBuffer.get() : nullptr; + } else { + rowPtr = mNext.AdvanceRow(); + } + + // If there's still more data coming or we're already done, just adjust the + // pointer and return. + if (mRow < mFrameRect.YMost() || rowPtr == nullptr) { + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect. Advance to the end + // of the next pipeline stage's buffer, outputting blank rows. + while (mNext.WriteEmptyRow() == WriteState::NEED_MORE_DATA) { + } + + mRow = mFrameRect.YMost(); + return nullptr; // We're done. + } + + private: + uint8_t* AdjustRowPointer(uint8_t* aNextRowPointer) const { + if (mBuffer) { + MOZ_ASSERT(aNextRowPointer == mBuffer.get() || + aNextRowPointer == nullptr); + return aNextRowPointer; // No adjustment needed for an intermediate + // buffer. + } + + if (mFrameRect.IsEmpty() || mRow >= mFrameRect.YMost() || + aNextRowPointer == nullptr) { + return nullptr; // Nothing left to write. + } + + return aNextRowPointer + mFrameRect.X() * sizeof(uint32_t); + } + + Next mNext; /// The next SurfaceFilter in the chain. + + gfx::IntRect mFrameRect; /// The surface subrect which contains data, + /// clamped to the image size. + gfx::IntRect mUnclampedFrameRect; /// The frame rect before clamping. + UniquePtr mBuffer; /// The intermediate buffer, if one is + /// necessary because the frame rect width + /// is larger than the image's logical width. + int32_t mRow; /// The row in unclamped frame rect space + /// that we're currently writing. +}; + +////////////////////////////////////////////////////////////////////////////// +// ADAM7InterpolatingFilter +////////////////////////////////////////////////////////////////////////////// + +template +class ADAM7InterpolatingFilter; + +/** + * A configuration struct for ADAM7InterpolatingFilter. + */ +struct ADAM7InterpolatingConfig { + template + using Filter = ADAM7InterpolatingFilter; +}; + +/** + * ADAM7InterpolatingFilter performs bilinear interpolation over an ADAM7 + * interlaced image. + * + * ADAM7 breaks up the image into 8x8 blocks. On each of the 7 passes, a new set + * of pixels in each block receives their final values, according to the + * following pattern: + * + * 1 6 4 6 2 6 4 6 + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * 3 6 4 6 3 6 4 6 + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * + * When rendering the pixels that have not yet received their final values, we + * can get much better intermediate results if we interpolate between + * the pixels we *have* gotten so far. This filter performs bilinear + * interpolation by first performing linear interpolation horizontally for each + * "important" row (which we'll define as a row that has received any pixels + * with final values at all) and then performing linear interpolation vertically + * to produce pixel values for rows which aren't important on the current pass. + * + * Note that this filter totally ignores the data which is written to rows which + * aren't important on the current pass! It's fine to write nothing at all for + * these rows, although doing so won't cause any harm. + * + * XXX(seth): In bug 1280552 we'll add a SIMD implementation for this filter. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template +class ADAM7InterpolatingFilter final : public SurfaceFilter { + public: + ADAM7InterpolatingFilter() + : mPass(0) // The current pass, in the range 1..7. Starts at 0 so that + // DoResetToFirstRow() doesn't have to special case the first + // pass. + , + mRow(0) {} + + template + nsresult Configure(const ADAM7InterpolatingConfig& aConfig, + const Rest&... aRest) { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + // We have two intermediate buffers, one for the previous row with final + // pixel values and one for the row that the previous filter in the chain is + // currently writing to. + size_t inputWidthInBytes = mNext.InputSize().width * sizeof(uint32_t); + mPreviousRow.reset(new (fallible) uint8_t[inputWidthInBytes]); + if (MOZ_UNLIKELY(!mPreviousRow)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mCurrentRow.reset(new (fallible) uint8_t[inputWidthInBytes]); + if (MOZ_UNLIKELY(!mCurrentRow)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memset(mPreviousRow.get(), 0, inputWidthInBytes); + memset(mCurrentRow.get(), 0, inputWidthInBytes); + + ConfigureFilter(mNext.InputSize(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe TakeInvalidRect() override { + return mNext.TakeInvalidRect(); + } + + protected: + uint8_t* DoResetToFirstRow() override { + mRow = 0; + mPass = std::min(mPass + 1, 7); + + uint8_t* rowPtr = mNext.ResetToFirstRow(); + if (mPass == 7) { + // Short circuit this filter on the final pass, since all pixels have + // their final values at that point. + return rowPtr; + } + + return mCurrentRow.get(); + } + + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) override { + CopyInputRow(aInputRow); + return DoAdvanceRow(); + } + + uint8_t* DoAdvanceRow() override { + MOZ_ASSERT(0 < mPass && mPass <= 7, "Invalid pass"); + + int32_t currentRow = mRow; + ++mRow; + + if (mPass == 7) { + // On the final pass we short circuit this filter totally. + return mNext.AdvanceRow(); + } + + const int32_t lastImportantRow = + LastImportantRow(InputSize().height, mPass); + if (currentRow > lastImportantRow) { + return nullptr; // This pass is already complete. + } + + if (!IsImportantRow(currentRow, mPass)) { + // We just ignore whatever the caller gives us for these rows. We'll + // interpolate them in later. + return mCurrentRow.get(); + } + + // This is an important row. We need to perform horizontal interpolation for + // these rows. + InterpolateHorizontally(mCurrentRow.get(), InputSize().width, mPass); + + // Interpolate vertically between the previous important row and the current + // important row. We skip this if the current row is 0 (which is always an + // important row), because in that case there is no previous important row + // to interpolate with. + if (currentRow != 0) { + InterpolateVertically(mPreviousRow.get(), mCurrentRow.get(), mPass, + mNext); + } + + // Write out the current row itself, which, being an important row, does not + // need vertical interpolation. + uint32_t* currentRowAsPixels = + reinterpret_cast(mCurrentRow.get()); + mNext.WriteBuffer(currentRowAsPixels); + + if (currentRow == lastImportantRow) { + // This is the last important row, which completes this pass. Note that + // for very small images, this may be the first row! Since there won't be + // another important row, there's nothing to interpolate with vertically, + // so we just duplicate this row until the end of the image. + while (mNext.WriteBuffer(currentRowAsPixels) == + WriteState::NEED_MORE_DATA) { + } + + // All of the remaining rows in the image were determined above, so we're + // done. + return nullptr; + } + + // The current row is now the previous important row; save it. + std::swap(mPreviousRow, mCurrentRow); + + MOZ_ASSERT(mRow < InputSize().height, + "Reached the end of the surface without " + "hitting the last important row?"); + + return mCurrentRow.get(); + } + + private: + static void InterpolateVertically(uint8_t* aPreviousRow, uint8_t* aCurrentRow, + uint8_t aPass, SurfaceFilter& aNext) { + const float* weights = InterpolationWeights(ImportantRowStride(aPass)); + + // We need to interpolate vertically to generate the rows between the + // previous important row and the next one. Recall that important rows are + // rows which contain at least some final pixels; see + // InterpolateHorizontally() for some additional explanation as to what that + // means. Note that we've already written out the previous important row, so + // we start the iteration at 1. + for (int32_t outRow = 1; outRow < ImportantRowStride(aPass); ++outRow) { + const float weight = weights[outRow]; + + // We iterate through the previous and current important row every time we + // write out an interpolated row, so we need to copy the pointers. + uint8_t* prevRowBytes = aPreviousRow; + uint8_t* currRowBytes = aCurrentRow; + + // Write out the interpolated pixels. Interpolation is componentwise. + aNext.template WritePixelsToRow([&] { + uint32_t pixel = 0; + auto* component = reinterpret_cast(&pixel); + *component++ = + InterpolateByte(*prevRowBytes++, *currRowBytes++, weight); + *component++ = + InterpolateByte(*prevRowBytes++, *currRowBytes++, weight); + *component++ = + InterpolateByte(*prevRowBytes++, *currRowBytes++, weight); + *component++ = + InterpolateByte(*prevRowBytes++, *currRowBytes++, weight); + return AsVariant(pixel); + }); + } + } + + static void InterpolateHorizontally(uint8_t* aRow, int32_t aWidth, + uint8_t aPass) { + // Collect the data we'll need to perform horizontal interpolation. The + // terminology here bears some explanation: a "final pixel" is a pixel which + // has received its final value. On each pass, a new set of pixels receives + // their final value; see the diagram above of the 8x8 pattern that ADAM7 + // uses. Any pixel which hasn't received its final value on this pass + // derives its value from either horizontal or vertical interpolation + // instead. + const size_t finalPixelStride = FinalPixelStride(aPass); + const size_t finalPixelStrideBytes = finalPixelStride * sizeof(uint32_t); + const size_t lastFinalPixel = LastFinalPixel(aWidth, aPass); + const size_t lastFinalPixelBytes = lastFinalPixel * sizeof(uint32_t); + const float* weights = InterpolationWeights(finalPixelStride); + + // Interpolate blocks of pixels which lie between two final pixels. + // Horizontal interpolation is done in place, as we'll need the results + // later when we vertically interpolate. + for (size_t blockBytes = 0; blockBytes < lastFinalPixelBytes; + blockBytes += finalPixelStrideBytes) { + uint8_t* finalPixelA = aRow + blockBytes; + uint8_t* finalPixelB = aRow + blockBytes + finalPixelStrideBytes; + + MOZ_ASSERT(finalPixelA < aRow + aWidth * sizeof(uint32_t), + "Running off end of buffer"); + MOZ_ASSERT(finalPixelB < aRow + aWidth * sizeof(uint32_t), + "Running off end of buffer"); + + // Interpolate the individual pixels componentwise. Note that we start + // iteration at 1 since we don't need to apply any interpolation to the + // first pixel in the block, which has its final value. + for (size_t pixelIndex = 1; pixelIndex < finalPixelStride; ++pixelIndex) { + const float weight = weights[pixelIndex]; + uint8_t* pixel = aRow + blockBytes + pixelIndex * sizeof(uint32_t); + + MOZ_ASSERT(pixel < aRow + aWidth * sizeof(uint32_t), + "Running off end of buffer"); + + for (size_t component = 0; component < sizeof(uint32_t); ++component) { + pixel[component] = InterpolateByte(finalPixelA[component], + finalPixelB[component], weight); + } + } + } + + // For the pixels after the last final pixel in the row, there isn't a + // second final pixel to interpolate with, so just duplicate. + uint32_t* rowPixels = reinterpret_cast(aRow); + uint32_t pixelToDuplicate = rowPixels[lastFinalPixel]; + for (int32_t pixelIndex = lastFinalPixel + 1; pixelIndex < aWidth; + ++pixelIndex) { + MOZ_ASSERT(pixelIndex < aWidth, "Running off end of buffer"); + rowPixels[pixelIndex] = pixelToDuplicate; + } + } + + static uint8_t InterpolateByte(uint8_t aByteA, uint8_t aByteB, + float aWeight) { + return uint8_t(aByteA * aWeight + aByteB * (1.0f - aWeight)); + } + + static int32_t ImportantRowStride(uint8_t aPass) { + MOZ_ASSERT(0 < aPass && aPass <= 7, "Invalid pass"); + + // The stride between important rows for each pass, with a dummy value for + // the nonexistent pass 0. + static int32_t strides[] = {1, 8, 8, 4, 4, 2, 2, 1}; + + return strides[aPass]; + } + + static bool IsImportantRow(int32_t aRow, uint8_t aPass) { + MOZ_ASSERT(aRow >= 0); + + // Whether the row is important comes down to divisibility by the stride for + // this pass, which is always a power of 2, so we can check using a mask. + int32_t mask = ImportantRowStride(aPass) - 1; + return (aRow & mask) == 0; + } + + static int32_t LastImportantRow(int32_t aHeight, uint8_t aPass) { + MOZ_ASSERT(aHeight > 0); + + // We can find the last important row using the same mask trick as above. + int32_t lastRow = aHeight - 1; + int32_t mask = ImportantRowStride(aPass) - 1; + return lastRow - (lastRow & mask); + } + + static size_t FinalPixelStride(uint8_t aPass) { + MOZ_ASSERT(0 < aPass && aPass <= 7, "Invalid pass"); + + // The stride between the final pixels in important rows for each pass, with + // a dummy value for the nonexistent pass 0. + static size_t strides[] = {1, 8, 4, 4, 2, 2, 1, 1}; + + return strides[aPass]; + } + + static size_t LastFinalPixel(int32_t aWidth, uint8_t aPass) { + MOZ_ASSERT(aWidth >= 0); + + // Again, we can use the mask trick above to find the last important pixel. + int32_t lastColumn = aWidth - 1; + size_t mask = FinalPixelStride(aPass) - 1; + return lastColumn - (lastColumn & mask); + } + + static const float* InterpolationWeights(int32_t aStride) { + // Precalculated interpolation weights. These are used to interpolate + // between final pixels or between important rows. Although no interpolation + // is actually applied to the previous final pixel or important row value, + // the arrays still start with 1.0f, which is always skipped, primarily + // because otherwise |stride1Weights| would have zero elements. + static float stride8Weights[] = {1.0f, 7 / 8.0f, 6 / 8.0f, 5 / 8.0f, + 4 / 8.0f, 3 / 8.0f, 2 / 8.0f, 1 / 8.0f}; + static float stride4Weights[] = {1.0f, 3 / 4.0f, 2 / 4.0f, 1 / 4.0f}; + static float stride2Weights[] = {1.0f, 1 / 2.0f}; + static float stride1Weights[] = {1.0f}; + + switch (aStride) { + case 8: + return stride8Weights; + case 4: + return stride4Weights; + case 2: + return stride2Weights; + case 1: + return stride1Weights; + default: + MOZ_CRASH(); + } + } + + Next mNext; /// The next SurfaceFilter in the chain. + + UniquePtr + mPreviousRow; /// The last important row (i.e., row with + /// final pixel values) that got written to. + UniquePtr mCurrentRow; /// The row that's being written to right + /// now. + uint8_t mPass; /// Which ADAM7 pass we're on. Valid passes + /// are 1..7 during processing and 0 prior + /// to configuration. + int32_t mRow; /// The row we're currently reading. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfaceFilters_h diff --git a/image/SurfaceFlags.h b/image/SurfaceFlags.h new file mode 100644 index 0000000000..a494ea15ff --- /dev/null +++ b/image/SurfaceFlags.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_SurfaceFlags_h +#define mozilla_image_SurfaceFlags_h + +#include "imgIContainer.h" +#include "mozilla/TypedEnumBits.h" + +namespace mozilla { +namespace image { + +/** + * Flags that change the output a decoder generates. Because different + * combinations of these flags result in logically different surfaces, these + * flags must be taken into account in SurfaceCache lookups. + */ +enum class SurfaceFlags : uint8_t { + NO_PREMULTIPLY_ALPHA = 1 << 0, + NO_COLORSPACE_CONVERSION = 1 << 1, + TO_SRGB_COLORSPACE = 1 << 2, + RECORD_BLOB = 1 << 3, +}; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SurfaceFlags) + +/** + * @return the default set of surface flags. + */ +inline SurfaceFlags DefaultSurfaceFlags() { return SurfaceFlags(); } + +/** + * Given a set of imgIContainer FLAG_* flags, returns a set of SurfaceFlags with + * the corresponding flags set. + */ +inline SurfaceFlags ToSurfaceFlags(uint32_t aFlags) { + SurfaceFlags flags = DefaultSurfaceFlags(); + if (aFlags & imgIContainer::FLAG_DECODE_NO_PREMULTIPLY_ALPHA) { + flags |= SurfaceFlags::NO_PREMULTIPLY_ALPHA; + } + if (aFlags & imgIContainer::FLAG_DECODE_NO_COLORSPACE_CONVERSION) { + flags |= SurfaceFlags::NO_COLORSPACE_CONVERSION; + } + if (aFlags & imgIContainer::FLAG_DECODE_TO_SRGB_COLORSPACE) { + flags |= SurfaceFlags::TO_SRGB_COLORSPACE; + } + if (aFlags & imgIContainer::FLAG_RECORD_BLOB) { + flags |= SurfaceFlags::RECORD_BLOB; + } + return flags; +} + +/** + * Given a set of SurfaceFlags, returns a set of imgIContainer FLAG_* flags with + * the corresponding flags set. + */ +inline uint32_t FromSurfaceFlags(SurfaceFlags aFlags) { + uint32_t flags = imgIContainer::DECODE_FLAGS_DEFAULT; + if (aFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA) { + flags |= imgIContainer::FLAG_DECODE_NO_PREMULTIPLY_ALPHA; + } + if (aFlags & SurfaceFlags::NO_COLORSPACE_CONVERSION) { + flags |= imgIContainer::FLAG_DECODE_NO_COLORSPACE_CONVERSION; + } + if (aFlags & SurfaceFlags::TO_SRGB_COLORSPACE) { + flags |= imgIContainer::FLAG_DECODE_TO_SRGB_COLORSPACE; + } + if (aFlags & SurfaceFlags::RECORD_BLOB) { + flags |= imgIContainer::FLAG_RECORD_BLOB; + } + return flags; +} + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfaceFlags_h diff --git a/image/SurfacePipe.cpp b/image/SurfacePipe.cpp new file mode 100644 index 0000000000..2e4a86daef --- /dev/null +++ b/image/SurfacePipe.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SurfacePipe.h" + +#include // for min + +#include "Decoder.h" + +namespace mozilla { +namespace image { + +using namespace gfx; + +using std::min; + +Maybe AbstractSurfaceSink::TakeInvalidRect() { + if (mInvalidRect.IsEmpty()) { + return Nothing(); + } + + SurfaceInvalidRect invalidRect; + invalidRect.mInputSpaceRect = invalidRect.mOutputSpaceRect = mInvalidRect; + + // Forget about the invalid rect we're returning. + mInvalidRect = OrientedIntRect(); + + return Some(invalidRect); +} + +uint8_t* AbstractSurfaceSink::DoResetToFirstRow() { + mRow = 0; + return GetRowPointer(); +} + +uint8_t* SurfaceSink::DoAdvanceRowFromBuffer(const uint8_t* aInputRow) { + CopyInputRow(aInputRow); + return DoAdvanceRow(); +} + +uint8_t* SurfaceSink::DoAdvanceRow() { + if (mRow >= uint32_t(InputSize().height)) { + return nullptr; + } + + // If we're vertically flipping the output, we need to flip the invalid rect. + // Since we're dealing with an axis-aligned rect, only the y coordinate needs + // to change. + int32_t invalidY = mFlipVertically ? InputSize().height - (mRow + 1) : mRow; + mInvalidRect.UnionRect(mInvalidRect, + OrientedIntRect(0, invalidY, InputSize().width, 1)); + + mRow = min(uint32_t(InputSize().height), mRow + 1); + + return mRow < uint32_t(InputSize().height) ? GetRowPointer() : nullptr; +} + +nsresult SurfaceSink::Configure(const SurfaceConfig& aConfig) { + IntSize surfaceSize = aConfig.mOutputSize; + + // Allocate the frame. + // XXX(seth): Once every Decoder subclass uses SurfacePipe, we probably want + // to allocate the frame directly here and get rid of Decoder::AllocateFrame + // altogether. + nsresult rv = aConfig.mDecoder->AllocateFrame(surfaceSize, aConfig.mFormat, + aConfig.mAnimParams); + if (NS_FAILED(rv)) { + return rv; + } + + mImageData = aConfig.mDecoder->mImageData; + mImageDataLength = aConfig.mDecoder->mImageDataLength; + mFlipVertically = aConfig.mFlipVertically; + + MOZ_ASSERT(mImageData); + MOZ_ASSERT(uint64_t(mImageDataLength) == uint64_t(surfaceSize.width) * + uint64_t(surfaceSize.height) * + sizeof(uint32_t)); + + ConfigureFilter(surfaceSize, sizeof(uint32_t)); + return NS_OK; +} + +uint8_t* SurfaceSink::GetRowPointer() const { + // If we're flipping vertically, reverse the order in which we traverse the + // rows. + uint32_t row = mFlipVertically ? InputSize().height - (mRow + 1) : mRow; + + uint8_t* rowPtr = mImageData + row * InputSize().width * sizeof(uint32_t); + + MOZ_ASSERT(rowPtr >= mImageData); + MOZ_ASSERT(rowPtr < mImageData + mImageDataLength); + MOZ_ASSERT(rowPtr + InputSize().width * sizeof(uint32_t) <= + mImageData + mImageDataLength); + + return rowPtr; +} + +uint8_t* ReorientSurfaceSink::DoAdvanceRowFromBuffer(const uint8_t* aInputRow) { + if (mRow >= uint32_t(InputSize().height)) { + return nullptr; + } + + IntRect dirty = mReorientFn(aInputRow, mRow, mImageData, mSurfaceSize, + mSurfaceSize.width * sizeof(uint32_t)); + auto orientedDirty = OrientedIntRect::FromUnknownRect(dirty); + mInvalidRect.UnionRect(mInvalidRect, orientedDirty); + + mRow = min(uint32_t(InputSize().height), mRow + 1); + + return mRow < uint32_t(InputSize().height) ? GetRowPointer() : nullptr; +} + +uint8_t* ReorientSurfaceSink::DoAdvanceRow() { + return DoAdvanceRowFromBuffer(mBuffer.get()); +} + +nsresult ReorientSurfaceSink::Configure(const ReorientSurfaceConfig& aConfig) { + mSurfaceSize = aConfig.mOutputSize.ToUnknownSize(); + + // Allocate the frame. + // XXX(seth): Once every Decoder subclass uses SurfacePipe, we probably want + // to allocate the frame directly here and get rid of Decoder::AllocateFrame + // altogether. + nsresult rv = + aConfig.mDecoder->AllocateFrame(mSurfaceSize, aConfig.mFormat, Nothing()); + if (NS_FAILED(rv)) { + return rv; + } + + // The filters above us need the unoriented size as the input. + auto inputSize = + aConfig.mOrientation.ToUnoriented(aConfig.mOutputSize).ToUnknownSize(); + mBuffer.reset(new (fallible) uint8_t[inputSize.width * sizeof(uint32_t)]); + if (MOZ_UNLIKELY(!mBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memset(mBuffer.get(), 0xFF, inputSize.width * sizeof(uint32_t)); + + mReorientFn = ReorientRow(aConfig.mOrientation); + MOZ_ASSERT(mReorientFn); + + mImageData = aConfig.mDecoder->mImageData; + mImageDataLength = aConfig.mDecoder->mImageDataLength; + + MOZ_ASSERT(mImageData); + MOZ_ASSERT(uint64_t(mImageDataLength) == uint64_t(mSurfaceSize.width) * + uint64_t(mSurfaceSize.height) * + sizeof(uint32_t)); + + ConfigureFilter(inputSize, sizeof(uint32_t)); + return NS_OK; +} + +uint8_t* ReorientSurfaceSink::GetRowPointer() const { return mBuffer.get(); } + +} // namespace image +} // namespace mozilla diff --git a/image/SurfacePipe.h b/image/SurfacePipe.h new file mode 100644 index 0000000000..7dd20276e5 --- /dev/null +++ b/image/SurfacePipe.h @@ -0,0 +1,848 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * A SurfacePipe is a pipeline that consists of a series of SurfaceFilters + * terminating in a SurfaceSink. Each SurfaceFilter transforms the image data in + * some way before the SurfaceSink ultimately writes it to the surface. This + * design allows for each transformation to be tested independently, for the + * transformations to be combined as needed to meet the needs of different + * situations, and for all image decoders to share the same code for these + * transformations. + * + * Writing to the SurfacePipe is done using lambdas that act as generator + * functions. Because the SurfacePipe machinery controls where the writes take + * place, a bug in an image decoder cannot cause a buffer overflow of the + * underlying surface. + */ + +#ifndef mozilla_image_SurfacePipe_h +#define mozilla_image_SurfacePipe_h + +#include + +#include + +#include "AnimationParams.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" + +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Variant.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Swizzle.h" +#include "nsDebug.h" +#include "Orientation.h" + +namespace mozilla { +namespace image { + +class Decoder; + +/** + * An invalid rect for a surface. Results are given both in the space of the + * input image (i.e., before any SurfaceFilters are applied) and in the space + * of the output surface (after all SurfaceFilters). + */ +struct SurfaceInvalidRect { + OrientedIntRect + mInputSpaceRect; /// The invalid rect in pre-SurfacePipe space. + OrientedIntRect + mOutputSpaceRect; /// The invalid rect in post-SurfacePipe space. +}; + +/** + * An enum used to allow the lambdas passed to WritePixels() to communicate + * their state to the caller. + */ +enum class WriteState : uint8_t { + NEED_MORE_DATA, /// The lambda ran out of data. + + FINISHED, /// The lambda is done writing to the surface; future writes + /// will fail. + + FAILURE /// The lambda encountered an error. The caller may recover + /// if possible and continue to write. (This never indicates + /// an error in the SurfacePipe machinery itself; it's only + /// generated by the lambdas.) +}; + +/** + * A template alias used to make the return value of WritePixels() lambdas + * (which may return either a pixel value or a WriteState) easier to specify. + */ +template +using NextPixel = Variant; + +/** + * SurfaceFilter is the abstract superclass of SurfacePipe pipeline stages. It + * implements the the code that actually writes to the surface - WritePixels() + * and the other Write*() methods - which are non-virtual for efficiency. + * + * SurfaceFilter's API is nonpublic; only SurfacePipe and other SurfaceFilters + * should use it. Non-SurfacePipe code should use the methods on SurfacePipe. + * + * To implement a SurfaceFilter, it's necessary to subclass SurfaceFilter and + * implement, at a minimum, the pure virtual methods. It's also necessary to + * define a Config struct with a Filter typedef member that identifies the + * matching SurfaceFilter class, and a Configure() template method. See an + * existing SurfaceFilter subclass, such as RemoveFrameRectFilter, for an + * example of how the Configure() method must be implemented. It takes a list of + * Config structs, passes the tail of the list to the next filter in the chain's + * Configure() method, and then uses the head of the list to configure itself. A + * SurfaceFilter's Configure() method must also call + * SurfaceFilter::ConfigureFilter() to provide the Write*() methods with the + * information they need to do their jobs. + */ +class SurfaceFilter { + public: + SurfaceFilter() : mRowPointer(nullptr), mCol(0), mPixelSize(0) {} + + virtual ~SurfaceFilter() {} + + /** + * Reset this surface to the first row. It's legal for this filter to throw + * away any previously written data at this point, as all rows must be written + * to on every pass. + * + * @return a pointer to the buffer for the first row. + */ + uint8_t* ResetToFirstRow() { + mCol = 0; + mRowPointer = DoResetToFirstRow(); + return mRowPointer; + } + + /** + * Called by WritePixels() to advance this filter to the next row. + * + * @return a pointer to the buffer for the next row, or nullptr to indicate + * that we've finished the entire surface. + */ + uint8_t* AdvanceRow() { + mCol = 0; + mRowPointer = DoAdvanceRow(); + return mRowPointer; + } + + /** + * Called by WriteBuffer() to advance this filter to the next row, if the + * supplied row is a full row. + * + * @return a pointer to the buffer for the next row, or nullptr to indicate + * that we've finished the entire surface. + */ + uint8_t* AdvanceRow(const uint8_t* aInputRow) { + mCol = 0; + mRowPointer = DoAdvanceRowFromBuffer(aInputRow); + return mRowPointer; + } + + /// @return a pointer to the buffer for the current row. + uint8_t* CurrentRowPointer() const { return mRowPointer; } + + /// @return true if we've finished writing to the surface. + bool IsSurfaceFinished() const { return mRowPointer == nullptr; } + + /// @return the input size this filter expects. + gfx::IntSize InputSize() const { return mInputSize; } + + /** + * Write pixels to the surface one at a time by repeatedly calling a lambda + * that yields pixels. WritePixels() is completely memory safe. + * + * Writing continues until every pixel in the surface has been written to + * (i.e., IsSurfaceFinished() returns true) or the lambda returns a WriteState + * which WritePixels() will return to the caller. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aFunc A lambda that functions as a generator, yielding the next + * pixel in the surface each time it's called. The lambda must + * return a NextPixel value. + * + * @return A WriteState value indicating the lambda generator's state. + * WritePixels() itself will return WriteState::FINISHED if writing + * has finished, regardless of the lambda's internal state. + */ + template + WriteState WritePixels(Func aFunc) { + Maybe result; + while ( + !(result = DoWritePixelsToRow(std::forward(aFunc)))) { + } + + return *result; + } + + /** + * Write pixels to the surface by calling a lambda which may write as many + * pixels as there is remaining to complete the row. It is not completely + * memory safe as it trusts the underlying decoder not to overrun the given + * buffer, however it is an acceptable tradeoff for performance. + * + * Writing continues until every pixel in the surface has been written to + * (i.e., IsSurfaceFinished() returns true) or the lambda returns a WriteState + * which WritePixelBlocks() will return to the caller. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aFunc A lambda that functions as a generator, yielding at most the + * maximum number of pixels requested. The lambda must accept a + * pointer argument to the first pixel to write, a maximum + * number of pixels to write as part of the block, and return a + * NextPixel value. + * + * @return A WriteState value indicating the lambda generator's state. + * WritePixelBlocks() itself will return WriteState::FINISHED if + * writing has finished, regardless of the lambda's internal state. + */ + template + WriteState WritePixelBlocks(Func aFunc) { + Maybe result; + while (!(result = DoWritePixelBlockToRow( + std::forward(aFunc)))) { + } + + return *result; + } + + /** + * A variant of WritePixels() that writes a single row of pixels to the + * surface one at a time by repeatedly calling a lambda that yields pixels. + * WritePixelsToRow() is completely memory safe. + * + * Writing continues until every pixel in the row has been written to. If the + * surface is complete at that pointer, WriteState::FINISHED is returned; + * otherwise, WritePixelsToRow() returns WriteState::NEED_MORE_DATA. The + * lambda can terminate writing early by returning a WriteState itself, which + * WritePixelsToRow() will return to the caller. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aFunc A lambda that functions as a generator, yielding the next + * pixel in the surface each time it's called. The lambda must + * return a NextPixel value. + * + * @return A WriteState value indicating the lambda generator's state. + * WritePixels() itself will return WriteState::FINISHED if writing + * the entire surface has finished, or WriteState::NEED_MORE_DATA if + * writing the row has finished, regardless of the lambda's internal + * state. + */ + template + WriteState WritePixelsToRow(Func aFunc) { + return DoWritePixelsToRow(std::forward(aFunc)) + .valueOr(WriteState::NEED_MORE_DATA); + } + + /** + * Write a row to the surface by copying from a buffer. This is bounds checked + * and memory safe with respect to the surface, but care must still be taken + * by the caller not to overread the source buffer. This variant of + * WriteBuffer() requires a source buffer which contains |mInputSize.width| + * pixels. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aSource A buffer to copy from. This buffer must be + * |mInputSize.width| pixels wide, which means + * |mInputSize.width * sizeof(PixelType)| bytes. May not be + * null. + * + * @return WriteState::FINISHED if the entire surface has been written to. + * Otherwise, returns WriteState::NEED_MORE_DATA. If a null |aSource| + * value is passed, returns WriteState::FAILURE. + */ + template + WriteState WriteBuffer(const PixelType* aSource) { + MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4); + MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t)); + MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t)); + + if (IsSurfaceFinished()) { + return WriteState::FINISHED; // Already done. + } + + if (MOZ_UNLIKELY(!aSource)) { + NS_WARNING("Passed a null pointer to WriteBuffer"); + return WriteState::FAILURE; + } + + AdvanceRow(reinterpret_cast(aSource)); + return IsSurfaceFinished() ? WriteState::FINISHED + : WriteState::NEED_MORE_DATA; + } + + /** + * Write a row to the surface by copying from a buffer. This is bounds checked + * and memory safe with respect to the surface, but care must still be taken + * by the caller not to overread the source buffer. This variant of + * WriteBuffer() reads at most @aLength pixels from the buffer and writes them + * to the row starting at @aStartColumn. Any pixels in columns before + * @aStartColumn or after the pixels copied from the buffer are cleared. + * + * Bounds checking failures produce warnings in debug builds because although + * the bounds checking maintains safety, this kind of failure could indicate a + * bug in the calling code. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aSource A buffer to copy from. This buffer must be @aLength pixels + * wide, which means |aLength * sizeof(PixelType)| bytes. May + * not be null. + * @param aStartColumn The column to start writing to in the row. Columns + * before this are cleared. + * @param aLength The number of bytes, at most, which may be copied from + * @aSource. Fewer bytes may be copied in practice due to + * bounds checking. + * + * @return WriteState::FINISHED if the entire surface has been written to. + * Otherwise, returns WriteState::NEED_MORE_DATA. If a null |aSource| + * value is passed, returns WriteState::FAILURE. + */ + template + WriteState WriteBuffer(const PixelType* aSource, const size_t aStartColumn, + const size_t aLength) { + MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4); + MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t)); + MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t)); + + if (IsSurfaceFinished()) { + return WriteState::FINISHED; // Already done. + } + + if (MOZ_UNLIKELY(!aSource)) { + NS_WARNING("Passed a null pointer to WriteBuffer"); + return WriteState::FAILURE; + } + + PixelType* dest = reinterpret_cast(mRowPointer); + + // Clear the area before |aStartColumn|. + const size_t prefixLength = + std::min(mInputSize.width, aStartColumn); + if (MOZ_UNLIKELY(prefixLength != aStartColumn)) { + NS_WARNING("Provided starting column is out-of-bounds in WriteBuffer"); + } + + memset(dest, 0, mInputSize.width * sizeof(PixelType)); + dest += prefixLength; + + // Write |aLength| pixels from |aSource| into the row, with bounds checking. + const size_t bufferLength = + std::min(mInputSize.width - prefixLength, aLength); + if (MOZ_UNLIKELY(bufferLength != aLength)) { + NS_WARNING("Provided buffer length is out-of-bounds in WriteBuffer"); + } + + memcpy(dest, aSource, bufferLength * sizeof(PixelType)); + dest += bufferLength; + + // Clear the rest of the row. + const size_t suffixLength = + mInputSize.width - (prefixLength + bufferLength); + memset(dest, 0, suffixLength * sizeof(PixelType)); + + AdvanceRow(); + + return IsSurfaceFinished() ? WriteState::FINISHED + : WriteState::NEED_MORE_DATA; + } + + /** + * Write an empty row to the surface. If some pixels have already been written + * to this row, they'll be discarded. + * + * @return WriteState::FINISHED if the entire surface has been written to. + * Otherwise, returns WriteState::NEED_MORE_DATA. + */ + WriteState WriteEmptyRow() { + if (IsSurfaceFinished()) { + return WriteState::FINISHED; // Already done. + } + + memset(mRowPointer, 0, mInputSize.width * mPixelSize); + AdvanceRow(); + + return IsSurfaceFinished() ? WriteState::FINISHED + : WriteState::NEED_MORE_DATA; + } + + /** + * Write a row to the surface by calling a lambda that uses a pointer to + * directly write to the row. This is unsafe because SurfaceFilter can't + * provide any bounds checking; that's up to the lambda itself. For this + * reason, the other Write*() methods should be preferred whenever it's + * possible to use them; WriteUnsafeComputedRow() should be used only when + * it's absolutely necessary to avoid extra copies or other performance + * penalties. + * + * This method should never be exposed to SurfacePipe consumers; it's strictly + * for use in SurfaceFilters. If external code needs this method, it should + * probably be turned into a SurfaceFilter. + * + * The template parameter PixelType must be uint8_t (for paletted surfaces) or + * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel + * size passed to ConfigureFilter(). + * + * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520, + * which means we can remove the PixelType template parameter from this + * method. + * + * @param aFunc A lambda that writes directly to the row. + * + * @return WriteState::FINISHED if the entire surface has been written to. + * Otherwise, returns WriteState::NEED_MORE_DATA. + */ + template + WriteState WriteUnsafeComputedRow(Func aFunc) { + MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4); + MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t)); + MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t)); + + if (IsSurfaceFinished()) { + return WriteState::FINISHED; // Already done. + } + + // Call the provided lambda with a pointer to the buffer for the current + // row. This is unsafe because we can't do any bounds checking; the lambda + // itself has to be responsible for that. + PixelType* rowPtr = reinterpret_cast(mRowPointer); + aFunc(rowPtr, mInputSize.width); + AdvanceRow(); + + return IsSurfaceFinished() ? WriteState::FINISHED + : WriteState::NEED_MORE_DATA; + } + + ////////////////////////////////////////////////////////////////////////////// + // Methods Subclasses Should Override + ////////////////////////////////////////////////////////////////////////////// + + /** + * @return a SurfaceInvalidRect representing the region of the surface that + * has been written to since the last time TakeInvalidRect() was + * called, or Nothing() if the region is empty (i.e. nothing has been + * written). + */ + virtual Maybe TakeInvalidRect() = 0; + + protected: + /** + * Called by ResetToFirstRow() to actually perform the reset. It's legal to + * throw away any previously written data at this point, as all rows must be + * written to on every pass. + */ + virtual uint8_t* DoResetToFirstRow() = 0; + + /** + * Called by AdvanceRow() to actually advance this filter to the next row. + * + * @param aInputRow The input row supplied by the decoder. + * + * @return a pointer to the buffer for the next row, or nullptr to indicate + * that we've finished the entire surface. + */ + virtual uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) = 0; + + /** + * Called by AdvanceRow() to actually advance this filter to the next row. + * + * @return a pointer to the buffer for the next row, or nullptr to indicate + * that we've finished the entire surface. + */ + virtual uint8_t* DoAdvanceRow() = 0; + + ////////////////////////////////////////////////////////////////////////////// + // Methods For Internal Use By Subclasses + ////////////////////////////////////////////////////////////////////////////// + + /** + * Called by subclasses' Configure() methods to initialize the configuration + * of this filter. After the filter is configured, calls ResetToFirstRow(). + * + * @param aInputSize The input size of this filter, in pixels. The previous + * filter in the chain will expect to write into rows + * |aInputSize.width| pixels wide. + * @param aPixelSize How large, in bytes, each pixel in the surface is. This + * should be either 1 for paletted images or 4 for BGRA/BGRX + * images. + */ + void ConfigureFilter(gfx::IntSize aInputSize, uint8_t aPixelSize) { + mInputSize = aInputSize; + mPixelSize = aPixelSize; + + ResetToFirstRow(); + } + + /** + * Called by subclasses' DoAdvanceRowFromBuffer() methods to copy a decoder + * supplied row buffer into its internal row pointer. Ideally filters at the + * top of the filter pipeline are able to consume the decoder row buffer + * directly without the extra copy prior to performing its transformation. + * + * @param aInputRow The input row supplied by the decoder. + */ + void CopyInputRow(const uint8_t* aInputRow) { + MOZ_ASSERT(aInputRow); + MOZ_ASSERT(mCol == 0); + memcpy(mRowPointer, aInputRow, mPixelSize * mInputSize.width); + } + + private: + /** + * An internal method used to implement WritePixelBlocks. This method writes + * up to the number of pixels necessary to complete the row and returns Some() + * if we either finished the entire surface or the lambda returned a + * WriteState indicating that we should return to the caller. If the row was + * successfully written without either of those things happening, it returns + * Nothing(), allowing WritePixelBlocks() to iterate to fill as many rows as + * possible. + */ + template + Maybe DoWritePixelBlockToRow(Func aFunc) { + MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4); + MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t)); + MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t)); + + if (IsSurfaceFinished()) { + return Some(WriteState::FINISHED); // We're already done. + } + + PixelType* rowPtr = reinterpret_cast(mRowPointer); + int32_t remainder = mInputSize.width - mCol; + auto [written, result] = aFunc(&rowPtr[mCol], remainder); + if (written == remainder) { + MOZ_ASSERT(result.isNothing()); + mCol = mInputSize.width; + AdvanceRow(); // We've finished the row. + return IsSurfaceFinished() ? Some(WriteState::FINISHED) : Nothing(); + } + + MOZ_ASSERT(written >= 0 && written < remainder); + MOZ_ASSERT(result.isSome()); + + mCol += written; + if (*result == WriteState::FINISHED) { + ZeroOutRestOfSurface(); + } + + return result; + } + + /** + * An internal method used to implement both WritePixels() and + * WritePixelsToRow(). Those methods differ only in their behavior after a row + * is successfully written - WritePixels() continues to write another row, + * while WritePixelsToRow() returns to the caller. This method writes a single + * row and returns Some() if we either finished the entire surface or the + * lambda returned a WriteState indicating that we should return to the + * caller. If the row was successfully written without either of those things + * happening, it returns Nothing(), allowing WritePixels() and + * WritePixelsToRow() to implement their respective behaviors. + */ + template + Maybe DoWritePixelsToRow(Func aFunc) { + MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4); + MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t)); + MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t)); + + if (IsSurfaceFinished()) { + return Some(WriteState::FINISHED); // We're already done. + } + + PixelType* rowPtr = reinterpret_cast(mRowPointer); + + for (; mCol < mInputSize.width; ++mCol) { + NextPixel result = aFunc(); + if (result.template is()) { + rowPtr[mCol] = result.template as(); + continue; + } + + switch (result.template as()) { + case WriteState::NEED_MORE_DATA: + return Some(WriteState::NEED_MORE_DATA); + + case WriteState::FINISHED: + ZeroOutRestOfSurface(); + return Some(WriteState::FINISHED); + + case WriteState::FAILURE: + // Note that we don't need to record this anywhere, because this + // indicates an error in aFunc, and there's nothing wrong with our + // machinery. The caller can recover as needed and continue writing to + // the row. + return Some(WriteState::FAILURE); + } + } + + AdvanceRow(); // We've finished the row. + + return IsSurfaceFinished() ? Some(WriteState::FINISHED) : Nothing(); + } + + template + void ZeroOutRestOfSurface() { + WritePixels([] { return AsVariant(PixelType(0)); }); + } + + gfx::IntSize mInputSize; /// The size of the input this filter expects. + uint8_t* mRowPointer; /// Pointer to the current row or null if finished. + int32_t mCol; /// The current column we're writing to. (0-indexed) + uint8_t mPixelSize; /// How large each pixel in the surface is, in bytes. +}; + +/** + * SurfacePipe is the public API that decoders should use to interact with a + * SurfaceFilter pipeline. + */ +class SurfacePipe { + public: + SurfacePipe() {} + + SurfacePipe(SurfacePipe&& aOther) : mHead(std::move(aOther.mHead)) {} + + ~SurfacePipe() {} + + SurfacePipe& operator=(SurfacePipe&& aOther) { + MOZ_ASSERT(this != &aOther); + mHead = std::move(aOther.mHead); + return *this; + } + + /// Begins a new pass, seeking to the first row of the surface. + void ResetToFirstRow() { + MOZ_ASSERT(mHead, "Use before configured!"); + mHead->ResetToFirstRow(); + } + + /** + * Write pixels to the surface one at a time by repeatedly calling a lambda + * that yields pixels. WritePixels() is completely memory safe. + * + * @see SurfaceFilter::WritePixels() for the canonical documentation. + */ + template + WriteState WritePixels(Func aFunc) { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WritePixels(std::forward(aFunc)); + } + + /** + * A variant of WritePixels() that writes up to a single row of pixels to the + * surface in blocks by repeatedly calling a lambda that yields up to the + * requested number of pixels. + * + * @see SurfaceFilter::WritePixelBlocks() for the canonical documentation. + */ + template + WriteState WritePixelBlocks(Func aFunc) { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WritePixelBlocks(std::forward(aFunc)); + } + + /** + * A variant of WritePixels() that writes a single row of pixels to the + * surface one at a time by repeatedly calling a lambda that yields pixels. + * WritePixelsToRow() is completely memory safe. + * + * @see SurfaceFilter::WritePixelsToRow() for the canonical documentation. + */ + template + WriteState WritePixelsToRow(Func aFunc) { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WritePixelsToRow(std::forward(aFunc)); + } + + /** + * Write a row to the surface by copying from a buffer. This is bounds checked + * and memory safe with respect to the surface, but care must still be taken + * by the caller not to overread the source buffer. This variant of + * WriteBuffer() requires a source buffer which contains |mInputSize.width| + * pixels. + * + * @see SurfaceFilter::WriteBuffer() for the canonical documentation. + */ + template + WriteState WriteBuffer(const PixelType* aSource) { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WriteBuffer(aSource); + } + + /** + * Write a row to the surface by copying from a buffer. This is bounds checked + * and memory safe with respect to the surface, but care must still be taken + * by the caller not to overread the source buffer. This variant of + * WriteBuffer() reads at most @aLength pixels from the buffer and writes them + * to the row starting at @aStartColumn. Any pixels in columns before + * @aStartColumn or after the pixels copied from the buffer are cleared. + * + * @see SurfaceFilter::WriteBuffer() for the canonical documentation. + */ + template + WriteState WriteBuffer(const PixelType* aSource, const size_t aStartColumn, + const size_t aLength) { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WriteBuffer(aSource, aStartColumn, aLength); + } + + /** + * Write an empty row to the surface. If some pixels have already been written + * to this row, they'll be discarded. + * + * @see SurfaceFilter::WriteEmptyRow() for the canonical documentation. + */ + WriteState WriteEmptyRow() { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->WriteEmptyRow(); + } + + /// @return true if we've finished writing to the surface. + bool IsSurfaceFinished() const { return mHead->IsSurfaceFinished(); } + + /// @see SurfaceFilter::TakeInvalidRect() for the canonical documentation. + Maybe TakeInvalidRect() const { + MOZ_ASSERT(mHead, "Use before configured!"); + return mHead->TakeInvalidRect(); + } + + private: + friend class SurfacePipeFactory; + friend class TestSurfacePipeFactory; + + explicit SurfacePipe(UniquePtr&& aHead) + : mHead(std::move(aHead)) {} + + SurfacePipe(const SurfacePipe&) = delete; + SurfacePipe& operator=(const SurfacePipe&) = delete; + + UniquePtr mHead; /// The first filter in the chain. +}; + +/** + * AbstractSurfaceSink contains shared implementation for both SurfaceSink and + * ReorientSurfaceSink. + */ +class AbstractSurfaceSink : public SurfaceFilter { + public: + AbstractSurfaceSink() + : mImageData(nullptr), + mImageDataLength(0), + mRow(0), + mFlipVertically(false) {} + + Maybe TakeInvalidRect() final; + + protected: + uint8_t* DoResetToFirstRow() final; + virtual uint8_t* GetRowPointer() const = 0; + + OrientedIntRect + mInvalidRect; /// The region of the surface that has been written + /// to since the last call to TakeInvalidRect(). + uint8_t* mImageData; /// A pointer to the beginning of the surface data. + uint32_t mImageDataLength; /// The length of the surface data. + uint32_t mRow; /// The row to which we're writing. (0-indexed) + bool mFlipVertically; /// If true, write the rows from top to bottom. +}; + +class SurfaceSink; + +/// A configuration struct for SurfaceSink. +struct SurfaceConfig { + using Filter = SurfaceSink; + Decoder* mDecoder; /// Which Decoder to use to allocate the surface. + gfx::IntSize mOutputSize; /// The size of the surface. + gfx::SurfaceFormat mFormat; /// The surface format (BGRA or BGRX). + bool mFlipVertically; /// If true, write the rows from bottom to top. + Maybe mAnimParams; /// Given for animated images. +}; + +/** + * A sink for surfaces. It handles the allocation of the surface and protects + * against buffer overflow. This sink should be used for most images. + * + * Sinks must always be at the end of the SurfaceFilter chain. + */ +class SurfaceSink final : public AbstractSurfaceSink { + public: + nsresult Configure(const SurfaceConfig& aConfig); + + protected: + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) final; + uint8_t* DoAdvanceRow() final; + uint8_t* GetRowPointer() const final; +}; + +class ReorientSurfaceSink; + +/// A configuration struct for ReorientSurfaceSink. +struct ReorientSurfaceConfig { + using Filter = ReorientSurfaceSink; + Decoder* mDecoder; /// Which Decoder to use to allocate the surface. + OrientedIntSize mOutputSize; /// The size of the surface. + gfx::SurfaceFormat mFormat; /// The surface format (BGRA or BGRX). + Orientation mOrientation; /// The desired orientation of the surface data. +}; + +/** + * A sink for surfaces. It handles the allocation of the surface and protects + * against buffer overflow. This sink should be used for images which have a + * non-identity orientation which we want to apply during decoding. + * + * Sinks must always be at the end of the SurfaceFilter chain. + */ +class ReorientSurfaceSink final : public AbstractSurfaceSink { + public: + nsresult Configure(const ReorientSurfaceConfig& aConfig); + + protected: + uint8_t* DoAdvanceRowFromBuffer(const uint8_t* aInputRow) final; + uint8_t* DoAdvanceRow() final; + uint8_t* GetRowPointer() const final; + + UniquePtr mBuffer; + gfx::ReorientRowFn mReorientFn; + gfx::IntSize mSurfaceSize; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfacePipe_h diff --git a/image/SurfacePipeFactory.h b/image/SurfacePipeFactory.h new file mode 100644 index 0000000000..004ce349a6 --- /dev/null +++ b/image/SurfacePipeFactory.h @@ -0,0 +1,679 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_SurfacePipeFactory_h +#define mozilla_image_SurfacePipeFactory_h + +#include "SurfacePipe.h" +#include "SurfaceFilters.h" + +namespace mozilla { +namespace image { + +namespace detail { + +/** + * FilterPipeline is a helper template for SurfacePipeFactory that determines + * the full type of the sequence of SurfaceFilters that a sequence of + * configuration structs corresponds to. To make this work, all configuration + * structs must include a typedef 'Filter' that identifies the SurfaceFilter + * they configure. + */ +template +struct FilterPipeline; + +template +struct FilterPipeline { + typedef typename Config::template Filter< + typename FilterPipeline::Type> + Type; +}; + +template +struct FilterPipeline { + typedef typename Config::Filter Type; +}; + +} // namespace detail + +/** + * Flags for SurfacePipeFactory, used in conjunction with the factory functions + * in SurfacePipeFactory to enable or disable various SurfacePipe + * functionality. + */ +enum class SurfacePipeFlags { + DEINTERLACE = 1 << 0, // If set, deinterlace the image. + + ADAM7_INTERPOLATE = + 1 << 1, // If set, the caller is deinterlacing the + // image using ADAM7, and we may want to + // interpolate it for better intermediate results. + + FLIP_VERTICALLY = 1 << 2, // If set, flip the image vertically. + + PROGRESSIVE_DISPLAY = 1 << 3, // If set, we expect the image to be displayed + // progressively. This enables features that + // result in a better user experience for + // progressive display but which may be more + // computationally expensive. + + PREMULTIPLY_ALPHA = 1 << 4, // If set, we want to premultiply the alpha + // channel and the individual color channels. +}; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SurfacePipeFlags) + +class SurfacePipeFactory { + public: + /** + * Creates and initializes a normal (i.e., non-paletted) SurfacePipe. + * + * @param aDecoder The decoder whose current frame the SurfacePipe will write + * to. + * @param aInputSize The original size of the image. + * @param aOutputSize The size the SurfacePipe should output. Must be the same + * as @aInputSize or smaller. If smaller, the image will be + * downscaled during decoding. + * @param aFrameRect The portion of the image that actually contains data. + * @param aFormat The surface format of the image; generally B8G8R8A8 or + * B8G8R8X8. + * @param aAnimParams Extra parameters used by animated images. + * @param aFlags Flags enabling or disabling various functionality for the + * SurfacePipe; see the SurfacePipeFlags documentation for more + * information. + * + * @return A SurfacePipe if the parameters allowed one to be created + * successfully, or Nothing() if the SurfacePipe could not be + * initialized. + */ + static Maybe CreateSurfacePipe( + Decoder* aDecoder, const OrientedIntSize& aInputSize, + const OrientedIntSize& aOutputSize, const OrientedIntRect& aFrameRect, + gfx::SurfaceFormat aInFormat, gfx::SurfaceFormat aOutFormat, + const Maybe& aAnimParams, qcms_transform* aTransform, + SurfacePipeFlags aFlags) { + const bool deinterlace = bool(aFlags & SurfacePipeFlags::DEINTERLACE); + const bool flipVertically = + bool(aFlags & SurfacePipeFlags::FLIP_VERTICALLY); + const bool progressiveDisplay = + bool(aFlags & SurfacePipeFlags::PROGRESSIVE_DISPLAY); + const bool downscale = aInputSize != aOutputSize; + const bool removeFrameRect = !aFrameRect.IsEqualEdges( + OrientedIntRect(OrientedIntPoint(0, 0), aInputSize)); + const bool blendAnimation = aAnimParams.isSome(); + const bool colorManagement = aTransform != nullptr; + const bool premultiplyAlpha = + bool(aFlags & SurfacePipeFlags::PREMULTIPLY_ALPHA); + + MOZ_ASSERT(aInFormat == gfx::SurfaceFormat::R8G8B8 || + aInFormat == gfx::SurfaceFormat::R8G8B8A8 || + aInFormat == gfx::SurfaceFormat::R8G8B8X8 || + aInFormat == gfx::SurfaceFormat::OS_RGBA || + aInFormat == gfx::SurfaceFormat::OS_RGBX); + + MOZ_ASSERT(aOutFormat == gfx::SurfaceFormat::OS_RGBA || + aOutFormat == gfx::SurfaceFormat::OS_RGBX); + + MOZ_ASSERT(aDecoder->GetOrientation().IsIdentity()); + + const bool inFormatRgb = aInFormat == gfx::SurfaceFormat::R8G8B8; + + const bool inFormatOpaque = aInFormat == gfx::SurfaceFormat::OS_RGBX || + aInFormat == gfx::SurfaceFormat::R8G8B8X8 || + inFormatRgb; + const bool outFormatOpaque = aOutFormat == gfx::SurfaceFormat::OS_RGBX; + + const bool inFormatOrder = aInFormat == gfx::SurfaceFormat::R8G8B8A8 || + aInFormat == gfx::SurfaceFormat::R8G8B8X8; + const bool outFormatOrder = aOutFormat == gfx::SurfaceFormat::R8G8B8A8 || + aOutFormat == gfx::SurfaceFormat::R8G8B8X8; + + // Early swizzles are for unpacking RGB or forcing RGBA/BGRA_U32 to + // RGBX/BGRX_U32. We should never want to premultiply in either case, + // because the image's alpha channel will always be opaque. This must be + // done before downscaling and color management. + bool unpackOrMaskSwizzle = + inFormatRgb || + (!inFormatOpaque && outFormatOpaque && inFormatOrder == outFormatOrder); + + // Late swizzles are for premultiplying RGBA/BGRA_U32 and/or possible + // converting between RGBA and BGRA_U32. It must happen after color + // management, and before downscaling. + bool swapOrAlphaSwizzle = + (!inFormatRgb && inFormatOrder != outFormatOrder) || premultiplyAlpha; + + if (unpackOrMaskSwizzle && swapOrAlphaSwizzle) { + MOZ_ASSERT_UNREACHABLE("Early and late swizzles not supported"); + return Nothing(); + } + + if (!unpackOrMaskSwizzle && !swapOrAlphaSwizzle && + aInFormat != aOutFormat) { + MOZ_ASSERT_UNREACHABLE("Need to swizzle, but not configured to"); + return Nothing(); + } + + // Don't interpolate if we're sure we won't show this surface to the user + // until it's completely decoded. The final pass of an ADAM7 image doesn't + // need interpolation, so we only need to interpolate if we'll be displaying + // the image while it's still being decoded. + const bool adam7Interpolate = + bool(aFlags & SurfacePipeFlags::ADAM7_INTERPOLATE) && + progressiveDisplay; + + if (deinterlace && adam7Interpolate) { + MOZ_ASSERT_UNREACHABLE("ADAM7 deinterlacing is handled by libpng"); + return Nothing(); + } + + // Construct configurations for the SurfaceFilters. Note that the order of + // these filters is significant. We want to deinterlace or interpolate raw + // input rows, before any other transformations, and we want to remove the + // frame rect (which may involve adding blank rows or columns to the image) + // before any downscaling, so that the new rows and columns are taken into + // account. + DeinterlacingConfig deinterlacingConfig{progressiveDisplay}; + ADAM7InterpolatingConfig interpolatingConfig; + RemoveFrameRectConfig removeFrameRectConfig{aFrameRect.ToUnknownRect()}; + BlendAnimationConfig blendAnimationConfig{aDecoder}; + DownscalingConfig downscalingConfig{aInputSize.ToUnknownSize(), aOutFormat}; + ColorManagementConfig colorManagementConfig{aTransform}; + SwizzleConfig swizzleConfig{aInFormat, aOutFormat, premultiplyAlpha}; + SurfaceConfig surfaceConfig{aDecoder, aOutputSize.ToUnknownSize(), + aOutFormat, flipVertically, aAnimParams}; + + Maybe pipe; + + if (unpackOrMaskSwizzle) { + if (colorManagement) { + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + removeFrameRectConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + removeFrameRectConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, removeFrameRectConfig, + downscalingConfig, colorManagementConfig, + surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + downscalingConfig, colorManagementConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + downscalingConfig, colorManagementConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + colorManagementConfig, blendAnimationConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + colorManagementConfig, blendAnimationConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, colorManagementConfig, + blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + colorManagementConfig, removeFrameRectConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + colorManagementConfig, removeFrameRectConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, colorManagementConfig, + removeFrameRectConfig, surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + colorManagementConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + colorManagementConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(swizzleConfig, colorManagementConfig, surfaceConfig); + } + } + } + } else { // (colorManagement is false) + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + downscalingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + downscalingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, downscalingConfig, surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + blendAnimationConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + blendAnimationConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(swizzleConfig, blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + removeFrameRectConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + removeFrameRectConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(swizzleConfig, removeFrameRectConfig, surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = + MakePipe(swizzleConfig, deinterlacingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = + MakePipe(swizzleConfig, interpolatingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, surfaceConfig); + } + } + } + } + } else if (swapOrAlphaSwizzle) { + if (colorManagement) { + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + deinterlacingConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + interpolatingConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, swizzleConfig, + removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + deinterlacingConfig, downscalingConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + interpolatingConfig, downscalingConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, swizzleConfig, + downscalingConfig, surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + deinterlacingConfig, blendAnimationConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + interpolatingConfig, blendAnimationConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, swizzleConfig, + blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + deinterlacingConfig, removeFrameRectConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + interpolatingConfig, removeFrameRectConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, swizzleConfig, + removeFrameRectConfig, surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + deinterlacingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(colorManagementConfig, swizzleConfig, + interpolatingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(colorManagementConfig, swizzleConfig, surfaceConfig); + } + } + } + } else { // (colorManagement is false) + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + downscalingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + downscalingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, downscalingConfig, surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + blendAnimationConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + blendAnimationConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(swizzleConfig, blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(swizzleConfig, deinterlacingConfig, + removeFrameRectConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(swizzleConfig, interpolatingConfig, + removeFrameRectConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = + MakePipe(swizzleConfig, removeFrameRectConfig, surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = + MakePipe(swizzleConfig, deinterlacingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = + MakePipe(swizzleConfig, interpolatingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(swizzleConfig, surfaceConfig); + } + } + } + } + } else { // (unpackOrMaskSwizzle and swapOrAlphaSwizzle are false) + if (colorManagement) { + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, + downscalingConfig, colorManagementConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, + downscalingConfig, colorManagementConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(removeFrameRectConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, downscalingConfig, + colorManagementConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(downscalingConfig, colorManagementConfig, + surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, colorManagementConfig, + blendAnimationConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, colorManagementConfig, + blendAnimationConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, blendAnimationConfig, + surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, colorManagementConfig, + removeFrameRectConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, colorManagementConfig, + removeFrameRectConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, removeFrameRectConfig, + surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, colorManagementConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, colorManagementConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(colorManagementConfig, surfaceConfig); + } + } + } + } else { // (colorManagement is false) + if (downscale) { + MOZ_ASSERT(!blendAnimation); + if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, + downscalingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(removeFrameRectConfig, downscalingConfig, + surfaceConfig); + } + } else { // (removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, downscalingConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, downscalingConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(downscalingConfig, surfaceConfig); + } + } + } else { // (downscale is false) + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, blendAnimationConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, blendAnimationConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, + surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, + surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(removeFrameRectConfig, surfaceConfig); + } + } else { // (blendAnimation and removeFrameRect is false) + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(surfaceConfig); + } + } + } + } + } + + return pipe; + } + + /** + * Creates and initializes a reorienting SurfacePipe. + * + * @param aDecoder The decoder whose current frame the SurfacePipe will write + * to. + * @param aInputSize The original size of the image. + * @param aOutputSize The size the SurfacePipe should output. Must be the same + * as @aInputSize or smaller. If smaller, the image will be + * downscaled during decoding. + * @param aFormat The surface format of the image; generally B8G8R8A8 or + * B8G8R8X8. + * @param aOrientation The orientation of the image. + * + * @return A SurfacePipe if the parameters allowed one to be created + * successfully, or Nothing() if the SurfacePipe could not be + * initialized. + */ + static Maybe CreateReorientSurfacePipe( + Decoder* aDecoder, const OrientedIntSize& aInputSize, + const OrientedIntSize& aOutputSize, gfx::SurfaceFormat aFormat, + qcms_transform* aTransform, const Orientation& aOrientation) { + const bool downscale = aInputSize != aOutputSize; + const bool colorManagement = aTransform != nullptr; + + // Construct configurations for the SurfaceFilters. Note that the order of + // these filters is significant. We want to deinterlace or interpolate raw + // input rows, before any other transformations, and we want to remove the + // frame rect (which may involve adding blank rows or columns to the image) + // before any downscaling, so that the new rows and columns are taken into + // account. + DownscalingConfig downscalingConfig{ + aOrientation.ToUnoriented(aInputSize).ToUnknownSize(), aFormat}; + ColorManagementConfig colorManagementConfig{aTransform}; + SurfaceConfig surfaceConfig{aDecoder, aOutputSize.ToUnknownSize(), aFormat, + /* mFlipVertically */ false, + /* mAnimParams */ Nothing()}; + ReorientSurfaceConfig reorientSurfaceConfig{aDecoder, aOutputSize, aFormat, + aOrientation}; + + Maybe pipe; + + if (aOrientation.IsIdentity()) { + if (colorManagement) { + if (downscale) { + pipe = + MakePipe(downscalingConfig, colorManagementConfig, surfaceConfig); + } else { // (downscale is false) + pipe = MakePipe(colorManagementConfig, surfaceConfig); + } + } else { // (colorManagement is false) + if (downscale) { + pipe = MakePipe(downscalingConfig, surfaceConfig); + } else { // (downscale is false) + pipe = MakePipe(surfaceConfig); + } + } + } else { // (orientation is not identity) + if (colorManagement) { + if (downscale) { + pipe = MakePipe(downscalingConfig, colorManagementConfig, + reorientSurfaceConfig); + } else { // (downscale is false) + pipe = MakePipe(colorManagementConfig, reorientSurfaceConfig); + } + } else { // (colorManagement is false) + if (downscale) { + pipe = MakePipe(downscalingConfig, reorientSurfaceConfig); + } else { // (downscale is false) + pipe = MakePipe(reorientSurfaceConfig); + } + } + } + + return pipe; + } + + private: + template + static Maybe MakePipe(const Configs&... aConfigs) { + auto pipe = MakeUnique::Type>(); + nsresult rv = pipe->Configure(aConfigs...); + if (NS_FAILED(rv)) { + return Nothing(); + } + + return Some(SurfacePipe{std::move(pipe)}); + } + + virtual ~SurfacePipeFactory() = 0; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_SurfacePipeFactory_h diff --git a/image/VectorImage.cpp b/image/VectorImage.cpp new file mode 100644 index 0000000000..dbef371306 --- /dev/null +++ b/image/VectorImage.cpp @@ -0,0 +1,1642 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "VectorImage.h" + +#include "AutoRestoreSVGState.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxDrawable.h" +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "imgFrame.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/MediaFeatureChange.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/SVGSVGElement.h" +#include "mozilla/dom/SVGDocument.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/PendingAnimationTracker.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/SVGObserverUtils.h" // for SVGRenderingObserver + +#include "nsIStreamListener.h" +#include "nsMimeTypes.h" +#include "nsPresContext.h" +#include "nsRect.h" +#include "nsString.h" +#include "nsStubDocumentObserver.h" +#include "nsWindowSizes.h" +#include "ImageRegion.h" +#include "ISurfaceProvider.h" +#include "LookupResult.h" +#include "Orientation.h" +#include "SVGDocumentWrapper.h" +#include "SVGDrawingCallback.h" +#include "SVGDrawingParameters.h" +#include "nsIDOMEventListener.h" +#include "SurfaceCache.h" +#include "BlobSurfaceProvider.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/image/Resolution.h" +#include "WindowRenderer.h" + +namespace mozilla { + +using namespace dom; +using namespace dom::SVGPreserveAspectRatio_Binding; +using namespace gfx; +using namespace layers; + +namespace image { + +// Helper-class: SVGRootRenderingObserver +class SVGRootRenderingObserver final : public SVGRenderingObserver { + public: + NS_DECL_ISUPPORTS + + SVGRootRenderingObserver(SVGDocumentWrapper* aDocWrapper, + VectorImage* aVectorImage) + : SVGRenderingObserver(), + mDocWrapper(aDocWrapper), + mVectorImage(aVectorImage), + mHonoringInvalidations(true) { + MOZ_ASSERT(mDocWrapper, "Need a non-null SVG document wrapper"); + MOZ_ASSERT(mVectorImage, "Need a non-null VectorImage"); + + StartObserving(); + Element* elem = GetReferencedElementWithoutObserving(); + MOZ_ASSERT(elem, "no root SVG node for us to observe"); + + SVGObserverUtils::AddRenderingObserver(elem, this); + mInObserverSet = true; + } + + void ResumeHonoringInvalidations() { mHonoringInvalidations = true; } + + protected: + virtual ~SVGRootRenderingObserver() { + // This needs to call our GetReferencedElementWithoutObserving override, + // so must be called here rather than in our base class's dtor. + StopObserving(); + } + + Element* GetReferencedElementWithoutObserving() final { + return mDocWrapper->GetRootSVGElem(); + } + + virtual void OnRenderingChange() override { + Element* elem = GetReferencedElementWithoutObserving(); + MOZ_ASSERT(elem, "missing root SVG node"); + + if (mHonoringInvalidations && !mDocWrapper->ShouldIgnoreInvalidation()) { + nsIFrame* frame = elem->GetPrimaryFrame(); + if (!frame || frame->PresShell()->IsDestroying()) { + // We're being destroyed. Bail out. + return; + } + + // Ignore further invalidations until we draw. + mHonoringInvalidations = false; + + mVectorImage->InvalidateObserversOnNextRefreshDriverTick(); + } + + // Our caller might've removed us from rendering-observer list. + // Add ourselves back! + if (!mInObserverSet) { + SVGObserverUtils::AddRenderingObserver(elem, this); + mInObserverSet = true; + } + } + + // Private data + const RefPtr mDocWrapper; + VectorImage* const mVectorImage; // Raw pointer because it owns me. + bool mHonoringInvalidations; +}; + +NS_IMPL_ISUPPORTS(SVGRootRenderingObserver, nsIMutationObserver) + +class SVGParseCompleteListener final : public nsStubDocumentObserver { + public: + NS_DECL_ISUPPORTS + + SVGParseCompleteListener(SVGDocument* aDocument, VectorImage* aImage) + : mDocument(aDocument), mImage(aImage) { + MOZ_ASSERT(mDocument, "Need an SVG document"); + MOZ_ASSERT(mImage, "Need an image"); + + mDocument->AddObserver(this); + } + + private: + ~SVGParseCompleteListener() { + if (mDocument) { + // The document must have been destroyed before we got our event. + // Otherwise this can't happen, since documents hold strong references to + // their observers. + Cancel(); + } + } + + public: + void EndLoad(Document* aDocument) override { + MOZ_ASSERT(aDocument == mDocument, "Got EndLoad for wrong document?"); + + // OnSVGDocumentParsed will release our owner's reference to us, so ensure + // we stick around long enough to complete our work. + RefPtr kungFuDeathGrip(this); + + mImage->OnSVGDocumentParsed(); + } + + void Cancel() { + MOZ_ASSERT(mDocument, "Duplicate call to Cancel"); + if (mDocument) { + mDocument->RemoveObserver(this); + mDocument = nullptr; + } + } + + private: + RefPtr mDocument; + VectorImage* const mImage; // Raw pointer to owner. +}; + +NS_IMPL_ISUPPORTS(SVGParseCompleteListener, nsIDocumentObserver) + +class SVGLoadEventListener final : public nsIDOMEventListener { + public: + NS_DECL_ISUPPORTS + + SVGLoadEventListener(Document* aDocument, VectorImage* aImage) + : mDocument(aDocument), mImage(aImage) { + MOZ_ASSERT(mDocument, "Need an SVG document"); + MOZ_ASSERT(mImage, "Need an image"); + + mDocument->AddEventListener(u"MozSVGAsImageDocumentLoad"_ns, this, true, + false); + } + + private: + ~SVGLoadEventListener() { + if (mDocument) { + // The document must have been destroyed before we got our event. + // Otherwise this can't happen, since documents hold strong references to + // their observers. + Cancel(); + } + } + + public: + NS_IMETHOD HandleEvent(Event* aEvent) override { + MOZ_ASSERT(mDocument, "Need an SVG document. Received multiple events?"); + + // OnSVGDocumentLoaded will release our owner's reference + // to us, so ensure we stick around long enough to complete our work. + RefPtr kungFuDeathGrip(this); + +#ifdef DEBUG + nsAutoString eventType; + aEvent->GetType(eventType); + MOZ_ASSERT(eventType.EqualsLiteral("MozSVGAsImageDocumentLoad"), + "Received unexpected event"); +#endif + + mImage->OnSVGDocumentLoaded(); + + return NS_OK; + } + + void Cancel() { + MOZ_ASSERT(mDocument, "Duplicate call to Cancel"); + if (mDocument) { + mDocument->RemoveEventListener(u"MozSVGAsImageDocumentLoad"_ns, this, + true); + mDocument = nullptr; + } + } + + private: + nsCOMPtr mDocument; + VectorImage* const mImage; // Raw pointer to owner. +}; + +NS_IMPL_ISUPPORTS(SVGLoadEventListener, nsIDOMEventListener) + +SVGDrawingCallback::SVGDrawingCallback(SVGDocumentWrapper* aSVGDocumentWrapper, + const IntSize& aViewportSize, + const IntSize& aSize, + uint32_t aImageFlags) + : mSVGDocumentWrapper(aSVGDocumentWrapper), + mViewportSize(aViewportSize), + mSize(aSize), + mImageFlags(aImageFlags) {} + +SVGDrawingCallback::~SVGDrawingCallback() = default; + +// Based loosely on SVGIntegrationUtils' PaintFrameCallback::operator() +bool SVGDrawingCallback::operator()(gfxContext* aContext, + const gfxRect& aFillRect, + const SamplingFilter aSamplingFilter, + const gfxMatrix& aTransform) { + MOZ_ASSERT(mSVGDocumentWrapper, "need an SVGDocumentWrapper"); + + // Get (& sanity-check) the helper-doc's presShell + RefPtr presShell = mSVGDocumentWrapper->GetPresShell(); + MOZ_ASSERT(presShell, "GetPresShell returned null for an SVG image?"); + + Document* doc = presShell->GetDocument(); + [[maybe_unused]] nsIURI* uri = doc ? doc->GetDocumentURI() : nullptr; + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING( + "SVG Image drawing", GRAPHICS, + nsPrintfCString("%dx%d %s", mSize.width, mSize.height, + uri ? uri->GetSpecOrDefault().get() : "N/A")); + + gfxContextAutoSaveRestore contextRestorer(aContext); + + // Clip to aFillRect so that we don't paint outside. + aContext->Clip(aFillRect); + + gfxMatrix matrix = aTransform; + if (!matrix.Invert()) { + return false; + } + aContext->SetMatrixDouble( + aContext->CurrentMatrixDouble().PreMultiply(matrix).PreScale( + double(mSize.width) / mViewportSize.width, + double(mSize.height) / mViewportSize.height)); + + nsPresContext* presContext = presShell->GetPresContext(); + MOZ_ASSERT(presContext, "pres shell w/out pres context"); + + nsRect svgRect(0, 0, presContext->DevPixelsToAppUnits(mViewportSize.width), + presContext->DevPixelsToAppUnits(mViewportSize.height)); + + RenderDocumentFlags renderDocFlags = + RenderDocumentFlags::IgnoreViewportScrolling; + if (!(mImageFlags & imgIContainer::FLAG_SYNC_DECODE)) { + renderDocFlags |= RenderDocumentFlags::AsyncDecodeImages; + } + if (mImageFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING) { + renderDocFlags |= RenderDocumentFlags::UseHighQualityScaling; + } + + presShell->RenderDocument(svgRect, renderDocFlags, + NS_RGBA(0, 0, 0, 0), // transparent + aContext); + + return true; +} + +// Implement VectorImage's nsISupports-inherited methods +NS_IMPL_ISUPPORTS(VectorImage, imgIContainer, nsIStreamListener, + nsIRequestObserver) + +//------------------------------------------------------------------------------ +// Constructor / Destructor + +VectorImage::VectorImage(nsIURI* aURI /* = nullptr */) + : ImageResource(aURI), // invoke superclass's constructor + mLockCount(0), + mIsInitialized(false), + mDiscardable(false), + mIsFullyLoaded(false), + mHaveAnimations(false), + mHasPendingInvalidation(false) {} + +VectorImage::~VectorImage() { + ReportDocumentUseCounters(); + CancelAllListeners(); + SurfaceCache::RemoveImage(ImageKey(this)); +} + +//------------------------------------------------------------------------------ +// Methods inherited from Image.h + +nsresult VectorImage::Init(const char* aMimeType, uint32_t aFlags) { + // We don't support re-initialization + if (mIsInitialized) { + return NS_ERROR_ILLEGAL_VALUE; + } + + MOZ_ASSERT(!mIsFullyLoaded && !mHaveAnimations && !mError, + "Flags unexpectedly set before initialization"); + MOZ_ASSERT(!strcmp(aMimeType, IMAGE_SVG_XML), "Unexpected mimetype"); + + mDiscardable = !!(aFlags & INIT_FLAG_DISCARDABLE); + + // Lock this image's surfaces in the SurfaceCache if we're not discardable. + if (!mDiscardable) { + mLockCount++; + SurfaceCache::LockImage(ImageKey(this)); + } + + mIsInitialized = true; + return NS_OK; +} + +size_t VectorImage::SizeOfSourceWithComputedFallback( + SizeOfState& aState) const { + if (!mSVGDocumentWrapper) { + return 0; // No document, so no memory used for the document. + } + + SVGDocument* doc = mSVGDocumentWrapper->GetDocument(); + if (!doc) { + return 0; // No document, so no memory used for the document. + } + + nsWindowSizes windowSizes(aState); + doc->DocAddSizeOfIncludingThis(windowSizes); + + if (windowSizes.getTotalSize() == 0) { + // MallocSizeOf fails on this platform. Because we also use this method for + // determining the size of cache entries, we need to return something + // reasonable here. Unfortunately, there's no way to estimate the document's + // size accurately, so we just use a constant value of 100KB, which will + // generally underestimate the true size. + return 100 * 1024; + } + + return windowSizes.getTotalSize(); +} + +nsresult VectorImage::OnImageDataComplete(nsIRequest* aRequest, + nsresult aStatus, bool aLastPart) { + // Call our internal OnStopRequest method, which only talks to our embedded + // SVG document. This won't have any effect on our ProgressTracker. + nsresult finalStatus = OnStopRequest(aRequest, aStatus); + + // Give precedence to Necko failure codes. + if (NS_FAILED(aStatus)) { + finalStatus = aStatus; + } + + Progress loadProgress = LoadCompleteProgress(aLastPart, mError, finalStatus); + + if (mIsFullyLoaded || mError) { + // Our document is loaded, so we're ready to notify now. + mProgressTracker->SyncNotifyProgress(loadProgress); + } else { + // Record our progress so far; we'll actually send the notifications in + // OnSVGDocumentLoaded or OnSVGDocumentError. + mLoadProgress = Some(loadProgress); + } + + return finalStatus; +} + +nsresult VectorImage::OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) { + return OnDataAvailable(aRequest, aInStr, aSourceOffset, aCount); +} + +nsresult VectorImage::StartAnimation() { + if (mError) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(ShouldAnimate(), "Should not animate!"); + + mSVGDocumentWrapper->StartAnimation(); + return NS_OK; +} + +nsresult VectorImage::StopAnimation() { + nsresult rv = NS_OK; + if (mError) { + rv = NS_ERROR_FAILURE; + } else { + MOZ_ASSERT(mIsFullyLoaded && mHaveAnimations, + "Should not have been animating!"); + + mSVGDocumentWrapper->StopAnimation(); + } + + mAnimating = false; + return rv; +} + +bool VectorImage::ShouldAnimate() { + return ImageResource::ShouldAnimate() && mIsFullyLoaded && mHaveAnimations; +} + +NS_IMETHODIMP_(void) +VectorImage::SetAnimationStartTime(const TimeStamp& aTime) { + // We don't care about animation start time. +} + +//------------------------------------------------------------------------------ +// imgIContainer methods + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetWidth(int32_t* aWidth) { + if (mError || !mIsFullyLoaded) { + // XXXdholbert Technically we should leave outparam untouched when we + // fail. But since many callers don't check for failure, we set it to 0 on + // failure, for sane/predictable results. + *aWidth = 0; + return NS_ERROR_FAILURE; + } + + SVGSVGElement* rootElem = mSVGDocumentWrapper->GetRootSVGElem(); + MOZ_ASSERT(rootElem, + "Should have a root SVG elem, since we finished " + "loading without errors"); + int32_t rootElemWidth = rootElem->GetIntrinsicWidth(); + if (rootElemWidth < 0) { + *aWidth = 0; + return NS_ERROR_FAILURE; + } + *aWidth = rootElemWidth; + return NS_OK; +} + +//****************************************************************************** +nsresult VectorImage::GetNativeSizes(nsTArray& aNativeSizes) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//****************************************************************************** +size_t VectorImage::GetNativeSizesLength() { return 0; } + +//****************************************************************************** +NS_IMETHODIMP_(void) +VectorImage::RequestRefresh(const TimeStamp& aTime) { + if (HadRecentRefresh(aTime)) { + return; + } + + Document* doc = mSVGDocumentWrapper->GetDocument(); + if (!doc) { + // We are racing between shutdown and a refresh. + return; + } + + PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); + if (tracker && ShouldAnimate()) { + tracker->TriggerPendingAnimationsOnNextTick(aTime); + } + + EvaluateAnimation(); + + mSVGDocumentWrapper->TickRefreshDriver(); + + if (mHasPendingInvalidation) { + SendInvalidationNotifications(); + } +} + +void VectorImage::SendInvalidationNotifications() { + // Animated images don't send out invalidation notifications as soon as + // they're generated. Instead, InvalidateObserversOnNextRefreshDriverTick + // records that there are pending invalidations and then returns immediately. + // The notifications are actually sent from RequestRefresh(). We send these + // notifications there to ensure that there is actually a document observing + // us. Otherwise, the notifications are just wasted effort. + // + // Non-animated images post an event to call this method from + // InvalidateObserversOnNextRefreshDriverTick, because RequestRefresh is never + // called for them. Ordinarily this isn't needed, since we send out + // invalidation notifications in OnSVGDocumentLoaded, but in rare cases the + // SVG document may not be 100% ready to render at that time. In those cases + // we would miss the subsequent invalidations if we didn't send out the + // notifications indirectly in |InvalidateObservers...|. + + mHasPendingInvalidation = false; + + if (SurfaceCache::InvalidateImage(ImageKey(this))) { + // If we still have recordings in the cache, make sure we handle future + // invalidations. + MOZ_ASSERT(mRenderingObserver, "Should have a rendering observer by now"); + mRenderingObserver->ResumeHonoringInvalidations(); + } + + if (mProgressTracker) { + mProgressTracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE, + GetMaxSizedIntRect()); + } +} + +NS_IMETHODIMP_(IntRect) +VectorImage::GetImageSpaceInvalidationRect(const IntRect& aRect) { + return aRect; +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetHeight(int32_t* aHeight) { + if (mError || !mIsFullyLoaded) { + // XXXdholbert Technically we should leave outparam untouched when we + // fail. But since many callers don't check for failure, we set it to 0 on + // failure, for sane/predictable results. + *aHeight = 0; + return NS_ERROR_FAILURE; + } + + SVGSVGElement* rootElem = mSVGDocumentWrapper->GetRootSVGElem(); + MOZ_ASSERT(rootElem, + "Should have a root SVG elem, since we finished " + "loading without errors"); + int32_t rootElemHeight = rootElem->GetIntrinsicHeight(); + if (rootElemHeight < 0) { + *aHeight = 0; + return NS_ERROR_FAILURE; + } + *aHeight = rootElemHeight; + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetIntrinsicSize(nsSize* aSize) { + if (mError || !mIsFullyLoaded) { + return NS_ERROR_FAILURE; + } + + nsIFrame* rootFrame = mSVGDocumentWrapper->GetRootLayoutFrame(); + if (!rootFrame) { + return NS_ERROR_FAILURE; + } + + *aSize = nsSize(-1, -1); + IntrinsicSize rfSize = rootFrame->GetIntrinsicSize(); + if (rfSize.width) { + aSize->width = *rfSize.width; + } + if (rfSize.height) { + aSize->height = *rfSize.height; + } + return NS_OK; +} + +//****************************************************************************** +Maybe VectorImage::GetIntrinsicRatio() { + if (mError || !mIsFullyLoaded) { + return Nothing(); + } + + nsIFrame* rootFrame = mSVGDocumentWrapper->GetRootLayoutFrame(); + if (!rootFrame) { + return Nothing(); + } + + return Some(rootFrame->GetIntrinsicRatio()); +} + +NS_IMETHODIMP_(Orientation) +VectorImage::GetOrientation() { return Orientation(); } + +NS_IMETHODIMP_(Resolution) +VectorImage::GetResolution() { return {}; } + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetType(uint16_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = imgIContainer::TYPE_VECTOR; + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetProviderId(uint32_t* aId) { + NS_ENSURE_ARG_POINTER(aId); + + *aId = ImageResource::GetImageProviderId(); + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::GetAnimated(bool* aAnimated) { + if (mError || !mIsFullyLoaded) { + return NS_ERROR_FAILURE; + } + + *aAnimated = mSVGDocumentWrapper->IsAnimated(); + return NS_OK; +} + +//****************************************************************************** +int32_t VectorImage::GetFirstFrameDelay() { + if (mError) { + return -1; + } + + if (!mSVGDocumentWrapper->IsAnimated()) { + return -1; + } + + // We don't really have a frame delay, so just pretend that we constantly + // need updates. + return 0; +} + +NS_IMETHODIMP_(bool) +VectorImage::WillDrawOpaqueNow() { + return false; // In general, SVG content is not opaque. +} + +//****************************************************************************** +NS_IMETHODIMP_(already_AddRefed) +VectorImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) { + if (mError) { + return nullptr; + } + + // Look up height & width + // ---------------------- + SVGSVGElement* svgElem = mSVGDocumentWrapper->GetRootSVGElem(); + MOZ_ASSERT(svgElem, + "Should have a root SVG elem, since we finished " + "loading without errors"); + nsIntSize imageIntSize(svgElem->GetIntrinsicWidth(), + svgElem->GetIntrinsicHeight()); + + if (imageIntSize.IsEmpty()) { + // We'll get here if our SVG doc has a percent-valued or negative width or + // height. + return nullptr; + } + + return GetFrameAtSize(imageIntSize, aWhichFrame, aFlags); +} + +NS_IMETHODIMP_(already_AddRefed) +VectorImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame, + uint32_t aFlags) { + MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE); + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + if (aSize.IsEmpty() || aWhichFrame > FRAME_MAX_VALUE || mError || + !mIsFullyLoaded) { + return nullptr; + } + + uint32_t whichFrame = mHaveAnimations ? aWhichFrame : FRAME_FIRST; + + auto [sourceSurface, decodeSize] = + LookupCachedSurface(aSize, SVGImageContext(), aFlags); + if (sourceSurface) { + return sourceSurface.forget(); + } + + if (mSVGDocumentWrapper->IsDrawing()) { + NS_WARNING("Refusing to make re-entrant call to VectorImage::Draw"); + return nullptr; + } + + float animTime = (whichFrame == FRAME_FIRST) + ? 0.0f + : mSVGDocumentWrapper->GetCurrentTimeAsFloat(); + + // By using a null gfxContext, we ensure that we will always attempt to + // create a surface, even if we aren't capable of caching it (e.g. due to our + // flags, having an animation, etc). Otherwise CreateSurface will assume that + // the caller is capable of drawing directly to its own draw target if we + // cannot cache. + SVGImageContext svgContext; + SVGDrawingParameters params( + nullptr, decodeSize, aSize, ImageRegion::Create(decodeSize), + SamplingFilter::POINT, svgContext, animTime, aFlags, 1.0); + + bool didCache; // Was the surface put into the cache? + + AutoRestoreSVGState autoRestore(params, mSVGDocumentWrapper, + /* aContextPaint */ false); + + RefPtr svgDrawable = CreateSVGDrawable(params); + RefPtr surface = CreateSurface(params, svgDrawable, didCache); + if (!surface) { + MOZ_ASSERT(!didCache); + return nullptr; + } + + SendFrameComplete(didCache, params.flags); + return surface.forget(); +} + +NS_IMETHODIMP_(bool) +VectorImage::IsImageContainerAvailable(WindowRenderer* aRenderer, + uint32_t aFlags) { + if (mError || !mIsFullyLoaded || + aRenderer->GetBackendType() != LayersBackend::LAYERS_WR) { + return false; + } + + if (mHaveAnimations && !StaticPrefs::image_svg_blob_image()) { + // We don't support rasterizing animation SVGs. We can put them in a blob + // recording however instead of using fallback. + return false; + } + + return true; +} + +//****************************************************************************** +NS_IMETHODIMP_(ImgDrawResult) +VectorImage::GetImageProvider(WindowRenderer* aRenderer, + const gfx::IntSize& aSize, + const SVGImageContext& aSVGContext, + const Maybe& aRegion, + uint32_t aFlags, + WebRenderImageProvider** aProvider) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRenderer); + MOZ_ASSERT(!(aFlags & FLAG_BYPASS_SURFACE_CACHE), "Unsupported flags"); + + // We don't need to check if the size is too big since we only support + // WebRender backends. + if (aSize.IsEmpty()) { + return ImgDrawResult::BAD_ARGS; + } + + if (mError) { + return ImgDrawResult::BAD_IMAGE; + } + + if (!mIsFullyLoaded) { + return ImgDrawResult::NOT_READY; + } + + if (mHaveAnimations && !(aFlags & FLAG_RECORD_BLOB)) { + // We don't support rasterizing animation SVGs. We can put them in a blob + // recording however instead of using fallback. + return ImgDrawResult::NOT_SUPPORTED; + } + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + // Only blob recordings support a region to restrict drawing. + const bool blobRecording = aFlags & FLAG_RECORD_BLOB; + MOZ_ASSERT_IF(!blobRecording, aRegion.isNothing()); + + LookupResult result(MatchType::NOT_FOUND); + auto playbackType = + mHaveAnimations ? PlaybackType::eAnimated : PlaybackType::eStatic; + auto surfaceFlags = ToSurfaceFlags(aFlags); + + SurfaceKey surfaceKey = + VectorSurfaceKey(aSize, aRegion, aSVGContext, surfaceFlags, playbackType); + if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) { + result = SurfaceCache::Lookup(ImageKey(this), surfaceKey, + /* aMarkUsed = */ true); + } else { + result = SurfaceCache::LookupBestMatch(ImageKey(this), surfaceKey, + /* aMarkUsed = */ true); + } + + // Unless we get a best match (exact or factor of 2 limited), then we want to + // generate a new recording/rerasterize, even if we have a substitute. + if (result && (result.Type() == MatchType::EXACT || + result.Type() == MatchType::SUBSTITUTE_BECAUSE_BEST)) { + result.Surface().TakeProvider(aProvider); + return ImgDrawResult::SUCCESS; + } + + // Ensure we store the surface with the correct key if we switched to factor + // of 2 sizing or we otherwise got clamped. + IntSize rasterSize(aSize); + if (!result.SuggestedSize().IsEmpty()) { + rasterSize = result.SuggestedSize(); + surfaceKey = surfaceKey.CloneWithSize(rasterSize); + } + + // We're about to rerasterize, which may mean that some of the previous + // surfaces we've rasterized aren't useful anymore. We can allow them to + // expire from the cache by unlocking them here, and then sending out an + // invalidation. If this image is locked, any surfaces that are still useful + // will become locked again when Draw touches them, and the remainder will + // eventually expire. + bool mayCache = SurfaceCache::CanHold(rasterSize); + if (mayCache) { + SurfaceCache::UnlockEntries(ImageKey(this)); + } + + // Blob recorded vector images just create a provider responsible for + // generating blob keys and recording bindings. The recording won't happen + // until the caller requests the key explicitly. + RefPtr provider; + if (blobRecording) { + provider = MakeRefPtr(ImageKey(this), surfaceKey, + mSVGDocumentWrapper, aFlags); + } else { + if (mSVGDocumentWrapper->IsDrawing()) { + NS_WARNING("Refusing to make re-entrant call to VectorImage::Draw"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + if (!SurfaceCache::IsLegalSize(rasterSize) || + !Factory::AllowedSurfaceSize(rasterSize)) { + // If either of these is true then the InitWithDrawable call below will + // fail, so fail early and use this opportunity to return NOT_SUPPORTED + // instead of TEMPORARY_ERROR as we do for any InitWithDrawable failure. + // This means that we will use fallback which has a path that will draw + // directly into the gfxContext without having to allocate a surface. It + // means we will have to use fallback and re-rasterize for everytime we + // have to draw this image, but it's better than not drawing anything at + // all. + return ImgDrawResult::NOT_SUPPORTED; + } + + // We aren't using blobs, so we need to rasterize. + float animTime = + mHaveAnimations ? mSVGDocumentWrapper->GetCurrentTimeAsFloat() : 0.0f; + + // By using a null gfxContext, we ensure that we will always attempt to + // create a surface, even if we aren't capable of caching it (e.g. due to + // our flags, having an animation, etc). Otherwise CreateSurface will assume + // that the caller is capable of drawing directly to its own draw target if + // we cannot cache. + SVGDrawingParameters params( + nullptr, rasterSize, aSize, ImageRegion::Create(rasterSize), + SamplingFilter::POINT, aSVGContext, animTime, aFlags, 1.0); + + RefPtr svgDrawable = CreateSVGDrawable(params); + bool contextPaint = aSVGContext.GetContextPaint(); + AutoRestoreSVGState autoRestore(params, mSVGDocumentWrapper, contextPaint); + + mSVGDocumentWrapper->UpdateViewportBounds(params.viewportSize); + mSVGDocumentWrapper->FlushImageTransformInvalidation(); + + // Given we have no context, the default backend is fine. + BackendType backend = + gfxPlatform::GetPlatform()->GetDefaultContentBackend(); + + // Try to create an imgFrame, initializing the surface it contains by + // drawing our gfxDrawable into it. (We use FILTER_NEAREST since we never + // scale here.) + auto frame = MakeNotNull>(); + nsresult rv = frame->InitWithDrawable( + svgDrawable, params.size, SurfaceFormat::OS_RGBA, SamplingFilter::POINT, + params.flags, backend); + + // If we couldn't create the frame, it was probably because it would end + // up way too big. Generally it also wouldn't fit in the cache, but the + // prefs could be set such that the cache isn't the limiting factor. + if (NS_FAILED(rv)) { + return ImgDrawResult::TEMPORARY_ERROR; + } + + provider = + MakeRefPtr(ImageKey(this), surfaceKey, frame); + } + + if (mayCache) { + // Attempt to cache the frame. + if (SurfaceCache::Insert(WrapNotNull(provider)) == InsertOutcome::SUCCESS) { + if (rasterSize != aSize) { + // We created a new surface that wasn't the size we requested, which + // means we entered factor-of-2 mode. We should purge any surfaces we + // no longer need rather than waiting for the cache to expire them. + SurfaceCache::PruneImage(ImageKey(this)); + } + + SendFrameComplete(/* aDidCache */ true, aFlags); + } + } + + MOZ_ASSERT(provider); + provider.forget(aProvider); + return ImgDrawResult::SUCCESS; +} + +bool VectorImage::MaybeRestrictSVGContext(SVGImageContext& aSVGContext, + uint32_t aFlags) { + bool overridePAR = (aFlags & FLAG_FORCE_PRESERVEASPECTRATIO_NONE); + + bool haveContextPaint = aSVGContext.GetContextPaint(); + bool blockContextPaint = + haveContextPaint && !SVGContextPaint::IsAllowedForImageFromURI(mURI); + + if (overridePAR || blockContextPaint) { + if (overridePAR) { + // The SVGImageContext must take account of the preserveAspectRatio + // override: + MOZ_ASSERT(!aSVGContext.GetPreserveAspectRatio(), + "FLAG_FORCE_PRESERVEASPECTRATIO_NONE is not expected if a " + "preserveAspectRatio override is supplied"); + Maybe aspectRatio = Some(SVGPreserveAspectRatio( + SVG_PRESERVEASPECTRATIO_NONE, SVG_MEETORSLICE_UNKNOWN)); + aSVGContext.SetPreserveAspectRatio(aspectRatio); + } + + if (blockContextPaint) { + // The SVGImageContext must not include context paint if the image is + // not allowed to use it: + aSVGContext.ClearContextPaint(); + } + } + + return haveContextPaint && !blockContextPaint; +} + +//****************************************************************************** +NS_IMETHODIMP_(ImgDrawResult) +VectorImage::Draw(gfxContext* aContext, const nsIntSize& aSize, + const ImageRegion& aRegion, uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + const SVGImageContext& aSVGContext, uint32_t aFlags, + float aOpacity) { + if (aWhichFrame > FRAME_MAX_VALUE) { + return ImgDrawResult::BAD_ARGS; + } + + if (!aContext) { + return ImgDrawResult::BAD_ARGS; + } + + if (mError) { + return ImgDrawResult::BAD_IMAGE; + } + + if (!mIsFullyLoaded) { + return ImgDrawResult::NOT_READY; + } + + if (mAnimationConsumers == 0 && mHaveAnimations) { + SendOnUnlockedDraw(aFlags); + } + + // We should bypass the cache when: + // - We are using a DrawTargetRecording because we prefer the drawing commands + // in general to the rasterized surface. This allows blob images to avoid + // rasterized SVGs with WebRender. + if (aContext->GetDrawTarget()->GetBackendType() == BackendType::RECORDING) { + aFlags |= FLAG_BYPASS_SURFACE_CACHE; + } + + MOZ_ASSERT(!(aFlags & FLAG_FORCE_PRESERVEASPECTRATIO_NONE) || + aSVGContext.GetViewportSize(), + "Viewport size is required when using " + "FLAG_FORCE_PRESERVEASPECTRATIO_NONE"); + + uint32_t whichFrame = mHaveAnimations ? aWhichFrame : FRAME_FIRST; + + float animTime = (whichFrame == FRAME_FIRST) + ? 0.0f + : mSVGDocumentWrapper->GetCurrentTimeAsFloat(); + + SVGImageContext newSVGContext = aSVGContext; + bool contextPaint = MaybeRestrictSVGContext(newSVGContext, aFlags); + + SVGDrawingParameters params(aContext, aSize, aSize, aRegion, aSamplingFilter, + newSVGContext, animTime, aFlags, aOpacity); + + // If we have an prerasterized version of this image that matches the + // drawing parameters, use that. + RefPtr sourceSurface; + std::tie(sourceSurface, params.size) = + LookupCachedSurface(aSize, params.svgContext, aFlags); + if (sourceSurface) { + RefPtr drawable = + new gfxSurfaceDrawable(sourceSurface, params.size); + Show(drawable, params); + return ImgDrawResult::SUCCESS; + } + + // else, we need to paint the image: + + if (mSVGDocumentWrapper->IsDrawing()) { + NS_WARNING("Refusing to make re-entrant call to VectorImage::Draw"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + AutoRestoreSVGState autoRestore(params, mSVGDocumentWrapper, contextPaint); + + bool didCache; // Was the surface put into the cache? + RefPtr svgDrawable = CreateSVGDrawable(params); + sourceSurface = CreateSurface(params, svgDrawable, didCache); + if (!sourceSurface) { + MOZ_ASSERT(!didCache); + Show(svgDrawable, params); + return ImgDrawResult::SUCCESS; + } + + RefPtr drawable = + new gfxSurfaceDrawable(sourceSurface, params.size); + Show(drawable, params); + SendFrameComplete(didCache, params.flags); + return ImgDrawResult::SUCCESS; +} + +already_AddRefed VectorImage::CreateSVGDrawable( + const SVGDrawingParameters& aParams) { + RefPtr cb = new SVGDrawingCallback( + mSVGDocumentWrapper, aParams.viewportSize, aParams.size, aParams.flags); + + RefPtr svgDrawable = new gfxCallbackDrawable(cb, aParams.size); + return svgDrawable.forget(); +} + +std::tuple, IntSize> VectorImage::LookupCachedSurface( + const IntSize& aSize, const SVGImageContext& aSVGContext, uint32_t aFlags) { + // We can't use cached surfaces if we: + // - Explicitly disallow it via FLAG_BYPASS_SURFACE_CACHE + // - Want a blob recording which aren't supported by the cache. + // - Have animations which aren't supported by the cache. + if (aFlags & (FLAG_BYPASS_SURFACE_CACHE | FLAG_RECORD_BLOB) || + mHaveAnimations) { + return std::make_tuple(RefPtr(), aSize); + } + + LookupResult result(MatchType::NOT_FOUND); + SurfaceKey surfaceKey = VectorSurfaceKey(aSize, aSVGContext); + if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) { + result = SurfaceCache::Lookup(ImageKey(this), surfaceKey, + /* aMarkUsed = */ true); + } else { + result = SurfaceCache::LookupBestMatch(ImageKey(this), surfaceKey, + /* aMarkUsed = */ true); + } + + IntSize rasterSize = + result.SuggestedSize().IsEmpty() ? aSize : result.SuggestedSize(); + MOZ_ASSERT(result.Type() != MatchType::SUBSTITUTE_BECAUSE_PENDING); + if (!result || result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND) { + // No matching surface, or the OS freed the volatile buffer. + return std::make_tuple(RefPtr(), rasterSize); + } + + RefPtr sourceSurface = result.Surface()->GetSourceSurface(); + if (!sourceSurface) { + // Something went wrong. (Probably a GPU driver crash or device reset.) + // Attempt to recover. + RecoverFromLossOfSurfaces(); + return std::make_tuple(RefPtr(), rasterSize); + } + + return std::make_tuple(std::move(sourceSurface), rasterSize); +} + +already_AddRefed VectorImage::CreateSurface( + const SVGDrawingParameters& aParams, gfxDrawable* aSVGDrawable, + bool& aWillCache) { + MOZ_ASSERT(mSVGDocumentWrapper->IsDrawing()); + MOZ_ASSERT(!(aParams.flags & FLAG_RECORD_BLOB)); + + mSVGDocumentWrapper->UpdateViewportBounds(aParams.viewportSize); + mSVGDocumentWrapper->FlushImageTransformInvalidation(); + + // Determine whether or not we should put the surface to be created into + // the cache. If we fail, we need to reset this to false to let the caller + // know nothing was put in the cache. + aWillCache = !(aParams.flags & FLAG_BYPASS_SURFACE_CACHE) && + // Refuse to cache animated images: + // XXX(seth): We may remove this restriction in bug 922893. + !mHaveAnimations && + // The image is too big to fit in the cache: + SurfaceCache::CanHold(aParams.size); + + // If we weren't given a context, then we know we just want the rasterized + // surface. We will create the frame below but only insert it into the cache + // if we actually need to. + if (!aWillCache && aParams.context) { + return nullptr; + } + + // We're about to rerasterize, which may mean that some of the previous + // surfaces we've rasterized aren't useful anymore. We can allow them to + // expire from the cache by unlocking them here, and then sending out an + // invalidation. If this image is locked, any surfaces that are still useful + // will become locked again when Draw touches them, and the remainder will + // eventually expire. + if (aWillCache) { + SurfaceCache::UnlockEntries(ImageKey(this)); + } + + // If there is no context, the default backend is fine. + BackendType backend = + aParams.context ? aParams.context->GetDrawTarget()->GetBackendType() + : gfxPlatform::GetPlatform()->GetDefaultContentBackend(); + + if (backend == BackendType::DIRECT2D1_1) { + // We don't want to draw arbitrary content with D2D anymore + // because it doesn't support PushLayerWithBlend so switch to skia + backend = BackendType::SKIA; + } + + // Try to create an imgFrame, initializing the surface it contains by drawing + // our gfxDrawable into it. (We use FILTER_NEAREST since we never scale here.) + auto frame = MakeNotNull>(); + nsresult rv = frame->InitWithDrawable( + aSVGDrawable, aParams.size, SurfaceFormat::OS_RGBA, SamplingFilter::POINT, + aParams.flags, backend); + + // If we couldn't create the frame, it was probably because it would end + // up way too big. Generally it also wouldn't fit in the cache, but the prefs + // could be set such that the cache isn't the limiting factor. + if (NS_FAILED(rv)) { + aWillCache = false; + return nullptr; + } + + // Take a strong reference to the frame's surface and make sure it hasn't + // already been purged by the operating system. + RefPtr surface = frame->GetSourceSurface(); + if (!surface) { + aWillCache = false; + return nullptr; + } + + // We created the frame, but only because we had no context to draw to + // directly. All the caller wants is the surface in this case. + if (!aWillCache) { + return surface.forget(); + } + + // Attempt to cache the frame. + SurfaceKey surfaceKey = VectorSurfaceKey(aParams.size, aParams.svgContext); + NotNull> provider = + MakeNotNull(ImageKey(this), surfaceKey, frame); + + if (SurfaceCache::Insert(provider) == InsertOutcome::SUCCESS) { + if (aParams.size != aParams.drawSize) { + // We created a new surface that wasn't the size we requested, which means + // we entered factor-of-2 mode. We should purge any surfaces we no longer + // need rather than waiting for the cache to expire them. + SurfaceCache::PruneImage(ImageKey(this)); + } + } else { + aWillCache = false; + } + + return surface.forget(); +} + +void VectorImage::SendFrameComplete(bool aDidCache, uint32_t aFlags) { + // If the cache was not updated, we have nothing to do. + if (!aDidCache) { + return; + } + + // Send out an invalidation so that surfaces that are still in use get + // re-locked. See the discussion of the UnlockSurfaces call above. + if (!(aFlags & FLAG_ASYNC_NOTIFY)) { + mProgressTracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE, + GetMaxSizedIntRect()); + } else { + NotNull> image = WrapNotNull(this); + NS_DispatchToMainThread(CreateRenderBlockingRunnable(NS_NewRunnableFunction( + "ProgressTracker::SyncNotifyProgress", [=]() -> void { + RefPtr tracker = image->GetProgressTracker(); + if (tracker) { + tracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE, + GetMaxSizedIntRect()); + } + }))); + } +} + +void VectorImage::Show(gfxDrawable* aDrawable, + const SVGDrawingParameters& aParams) { + // The surface size may differ from the size at which we wish to draw. As + // such, we may need to adjust the context/region to take this into account. + gfxContextMatrixAutoSaveRestore saveMatrix(aParams.context); + ImageRegion region(aParams.region); + if (aParams.drawSize != aParams.size) { + gfx::MatrixScales scale( + double(aParams.drawSize.width) / aParams.size.width, + double(aParams.drawSize.height) / aParams.size.height); + aParams.context->Multiply(gfx::Matrix::Scaling(scale)); + region.Scale(1.0 / scale.xScale, 1.0 / scale.yScale); + } + + MOZ_ASSERT(aDrawable, "Should have a gfxDrawable by now"); + gfxUtils::DrawPixelSnapped(aParams.context, aDrawable, + SizeDouble(aParams.size), region, + SurfaceFormat::OS_RGBA, aParams.samplingFilter, + aParams.flags, aParams.opacity, false); + + AutoProfilerImagePaintMarker PROFILER_RAII(this); +#ifdef DEBUG + NotifyDrawingObservers(); +#endif + + MOZ_ASSERT(mRenderingObserver, "Should have a rendering observer by now"); + mRenderingObserver->ResumeHonoringInvalidations(); +} + +void VectorImage::RecoverFromLossOfSurfaces() { + NS_WARNING("An imgFrame became invalid. Attempting to recover..."); + + // Discard all existing frames, since they're probably all now invalid. + SurfaceCache::RemoveImage(ImageKey(this)); +} + +NS_IMETHODIMP +VectorImage::StartDecoding(uint32_t aFlags, uint32_t aWhichFrame) { + // Nothing to do for SVG images + return NS_OK; +} + +bool VectorImage::StartDecodingWithResult(uint32_t aFlags, + uint32_t aWhichFrame) { + // SVG images are ready to draw when they are loaded + return mIsFullyLoaded; +} + +bool VectorImage::HasDecodedPixels() { + MOZ_ASSERT_UNREACHABLE("calling VectorImage::HasDecodedPixels"); + return mIsFullyLoaded; +} + +imgIContainer::DecodeResult VectorImage::RequestDecodeWithResult( + uint32_t aFlags, uint32_t aWhichFrame) { + // SVG images are ready to draw when they are loaded and don't have an error. + + if (mError) { + return imgIContainer::DECODE_REQUEST_FAILED; + } + + if (!mIsFullyLoaded) { + return imgIContainer::DECODE_REQUESTED; + } + + return imgIContainer::DECODE_SURFACE_AVAILABLE; +} + +NS_IMETHODIMP +VectorImage::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags, + uint32_t aWhichFrame) { + // Nothing to do for SVG images, though in theory we could rasterize to the + // provided size ahead of time if we supported off-main-thread SVG + // rasterization... + return NS_OK; +} + +//****************************************************************************** + +NS_IMETHODIMP +VectorImage::LockImage() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + return NS_ERROR_FAILURE; + } + + mLockCount++; + + if (mLockCount == 1) { + // Lock this image's surfaces in the SurfaceCache. + SurfaceCache::LockImage(ImageKey(this)); + } + + return NS_OK; +} + +//****************************************************************************** + +NS_IMETHODIMP +VectorImage::UnlockImage() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mError) { + return NS_ERROR_FAILURE; + } + + if (mLockCount == 0) { + MOZ_ASSERT_UNREACHABLE("Calling UnlockImage with a zero lock count"); + return NS_ERROR_ABORT; + } + + mLockCount--; + + if (mLockCount == 0) { + // Unlock this image's surfaces in the SurfaceCache. + SurfaceCache::UnlockImage(ImageKey(this)); + } + + return NS_OK; +} + +//****************************************************************************** + +NS_IMETHODIMP +VectorImage::RequestDiscard() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mDiscardable && mLockCount == 0) { + SurfaceCache::RemoveImage(ImageKey(this)); + mProgressTracker->OnDiscard(); + } + + return NS_OK; +} + +void VectorImage::OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) { + MOZ_ASSERT(mProgressTracker); + + NS_DispatchToMainThread(NewRunnableMethod("ProgressTracker::OnDiscard", + mProgressTracker, + &ProgressTracker::OnDiscard)); +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::ResetAnimation() { + if (mError) { + return NS_ERROR_FAILURE; + } + + if (!mIsFullyLoaded || !mHaveAnimations) { + return NS_OK; // There are no animations to be reset. + } + + mSVGDocumentWrapper->ResetAnimation(); + + return NS_OK; +} + +NS_IMETHODIMP_(float) +VectorImage::GetFrameIndex(uint32_t aWhichFrame) { + MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument"); + return aWhichFrame == FRAME_FIRST + ? 0.0f + : mSVGDocumentWrapper->GetCurrentTimeAsFloat(); +} + +//------------------------------------------------------------------------------ +// nsIRequestObserver methods + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(!mSVGDocumentWrapper, + "Repeated call to OnStartRequest -- can this happen?"); + + mSVGDocumentWrapper = new SVGDocumentWrapper(); + nsresult rv = mSVGDocumentWrapper->OnStartRequest(aRequest); + if (NS_FAILED(rv)) { + mSVGDocumentWrapper = nullptr; + mError = true; + return rv; + } + + // Create a listener to wait until the SVG document is fully loaded, which + // will signal that this image is ready to render. Certain error conditions + // will prevent us from ever getting this notification, so we also create a + // listener that waits for parsing to complete and cancels the + // SVGLoadEventListener if needed. The listeners are automatically attached + // to the document by their constructors. + SVGDocument* document = mSVGDocumentWrapper->GetDocument(); + mLoadEventListener = new SVGLoadEventListener(document, this); + mParseCompleteListener = new SVGParseCompleteListener(document, this); + + // Displayed documents will call InitUseCounters under SetScriptGlobalObject, + // but SVG image documents never get a script global object, so we initialize + // use counters here, right after the document has been created. + document->InitUseCounters(); + + return NS_OK; +} + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + if (mError) { + return NS_ERROR_FAILURE; + } + + return mSVGDocumentWrapper->OnStopRequest(aRequest, aStatus); +} + +void VectorImage::OnSVGDocumentParsed() { + MOZ_ASSERT(mParseCompleteListener, "Should have the parse complete listener"); + MOZ_ASSERT(mLoadEventListener, "Should have the load event listener"); + + if (!mSVGDocumentWrapper->GetRootSVGElem()) { + // This is an invalid SVG document. It may have failed to parse, or it may + // be missing the root element, or the root element may not + // declare the correct namespace. In any of these cases, we'll never be + // notified that the SVG finished loading, so we need to treat this as an + // error. + OnSVGDocumentError(); + } +} + +void VectorImage::CancelAllListeners() { + if (mParseCompleteListener) { + mParseCompleteListener->Cancel(); + mParseCompleteListener = nullptr; + } + if (mLoadEventListener) { + mLoadEventListener->Cancel(); + mLoadEventListener = nullptr; + } +} + +void VectorImage::OnSVGDocumentLoaded() { + MOZ_ASSERT(mSVGDocumentWrapper->GetRootSVGElem(), + "Should have parsed successfully"); + MOZ_ASSERT(!mIsFullyLoaded && !mHaveAnimations, + "These flags shouldn't get set until OnSVGDocumentLoaded. " + "Duplicate calls to OnSVGDocumentLoaded?"); + + CancelAllListeners(); + + // XXX Flushing is wasteful if embedding frame hasn't had initial reflow. + mSVGDocumentWrapper->FlushLayout(); + + // This is the earliest point that we can get accurate use counter data + // for a valid SVG document. Without the FlushLayout call, we would miss + // any CSS property usage that comes from SVG presentation attributes. + mSVGDocumentWrapper->GetDocument()->ReportDocumentUseCounters(); + + mIsFullyLoaded = true; + mHaveAnimations = mSVGDocumentWrapper->IsAnimated(); + + // Start listening to our image for rendering updates. + mRenderingObserver = new SVGRootRenderingObserver(mSVGDocumentWrapper, this); + + // ProgressTracker::SyncNotifyProgress may release us, so ensure we + // stick around long enough to complete our work. + RefPtr kungFuDeathGrip(this); + + // Tell *our* observers that we're done loading. + if (mProgressTracker) { + Progress progress = FLAG_SIZE_AVAILABLE | FLAG_HAS_TRANSPARENCY | + FLAG_FRAME_COMPLETE | FLAG_DECODE_COMPLETE; + + if (mHaveAnimations) { + progress |= FLAG_IS_ANIMATED; + } + + // Merge in any saved progress from OnImageDataComplete. + if (mLoadProgress) { + progress |= *mLoadProgress; + mLoadProgress = Nothing(); + } + + mProgressTracker->SyncNotifyProgress(progress, GetMaxSizedIntRect()); + } + + EvaluateAnimation(); +} + +void VectorImage::OnSVGDocumentError() { + CancelAllListeners(); + + mError = true; + + // We won't enter OnSVGDocumentLoaded, so report use counters now for this + // invalid document. + ReportDocumentUseCounters(); + + if (mProgressTracker) { + // Notify observers about the error and unblock page load. + Progress progress = FLAG_HAS_ERROR; + + // Merge in any saved progress from OnImageDataComplete. + if (mLoadProgress) { + progress |= *mLoadProgress; + mLoadProgress = Nothing(); + } + + mProgressTracker->SyncNotifyProgress(progress); + } +} + +//------------------------------------------------------------------------------ +// nsIStreamListener method + +//****************************************************************************** +NS_IMETHODIMP +VectorImage::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInStr, + uint64_t aSourceOffset, uint32_t aCount) { + if (mError) { + return NS_ERROR_FAILURE; + } + + return mSVGDocumentWrapper->OnDataAvailable(aRequest, aInStr, aSourceOffset, + aCount); +} + +// -------------------------- +// Invalidation helper method + +void VectorImage::InvalidateObserversOnNextRefreshDriverTick() { + if (mHasPendingInvalidation) { + return; + } + + mHasPendingInvalidation = true; + + // Animated images can wait for the refresh tick. + if (mHaveAnimations) { + return; + } + + // Non-animated images won't get the refresh tick, so we should just send an + // invalidation outside the current execution context. We need to defer + // because the layout tree is in the middle of invalidation, and the tree + // state needs to be consistent. Specifically only some of the frames have + // had the NS_FRAME_DESCENDANT_NEEDS_PAINT and/or NS_FRAME_NEEDS_PAINT bits + // set by InvalidateFrameInternal in layout/generic/nsFrame.cpp. These bits + // get cleared when we repaint the SVG into a surface by + // nsIFrame::ClearInvalidationStateBits in nsDisplayList::PaintRoot. + nsCOMPtr eventTarget; + if (mProgressTracker) { + eventTarget = mProgressTracker->GetEventTarget(); + } else { + eventTarget = do_GetMainThread(); + } + + RefPtr self(this); + nsCOMPtr ev(NS_NewRunnableFunction( + "VectorImage::SendInvalidationNotifications", + [=]() -> void { self->SendInvalidationNotifications(); })); + eventTarget->Dispatch(CreateRenderBlockingRunnable(ev.forget()), + NS_DISPATCH_NORMAL); +} + +void VectorImage::PropagateUseCounters(Document* aReferencingDocument) { + if (Document* doc = mSVGDocumentWrapper->GetDocument()) { + doc->PropagateImageUseCounters(aReferencingDocument); + } +} + +nsIntSize VectorImage::OptimalImageSizeForDest(const gfxSize& aDest, + uint32_t aWhichFrame, + SamplingFilter aSamplingFilter, + uint32_t aFlags) { + MOZ_ASSERT(aDest.width >= 0 || ceil(aDest.width) <= INT32_MAX || + aDest.height >= 0 || ceil(aDest.height) <= INT32_MAX, + "Unexpected destination size"); + + // We can rescale SVGs freely, so just return the provided destination size. + return nsIntSize::Ceil(aDest.width, aDest.height); +} + +already_AddRefed VectorImage::Unwrap() { + nsCOMPtr self(this); + return self.forget(); +} + +void VectorImage::MediaFeatureValuesChangedAllDocuments( + const MediaFeatureChange& aChange) { + if (!mSVGDocumentWrapper) { + return; + } + + // Don't bother if the document hasn't loaded yet. + if (!mIsFullyLoaded) { + return; + } + + if (Document* doc = mSVGDocumentWrapper->GetDocument()) { + if (RefPtr presContext = doc->GetPresContext()) { + presContext->MediaFeatureValuesChanged( + aChange, MediaFeatureChangePropagation::All); + // Media feature value changes don't happen in the middle of layout, + // so we don't need to call InvalidateObserversOnNextRefreshDriverTick + // to invalidate asynchronously. + if (presContext->FlushPendingMediaFeatureValuesChanged()) { + // NOTE(emilio): SendInvalidationNotifications flushes layout via + // VectorImage::CreateSurface -> FlushImageTransformInvalidation. + SendInvalidationNotifications(); + } + } + } +} + +nsresult VectorImage::GetHotspotX(int32_t* aX) { + return Image::GetHotspotX(aX); +} + +nsresult VectorImage::GetHotspotY(int32_t* aY) { + return Image::GetHotspotY(aY); +} + +void VectorImage::ReportDocumentUseCounters() { + if (!mSVGDocumentWrapper) { + return; + } + + if (Document* doc = mSVGDocumentWrapper->GetDocument()) { + doc->ReportDocumentUseCounters(); + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/VectorImage.h b/image/VectorImage.h new file mode 100644 index 0000000000..fc7df04483 --- /dev/null +++ b/image/VectorImage.h @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_VectorImage_h +#define mozilla_image_VectorImage_h + +#include "Image.h" +#include "nsIStreamListener.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/MemoryReporting.h" + +class nsIRequest; +class gfxDrawable; + +namespace mozilla { +struct MediaFeatureChange; + +namespace image { + +class SourceSurfaceBlobImage; +struct SVGDrawingParameters; +class SVGDocumentWrapper; +class SVGRootRenderingObserver; +class SVGLoadEventListener; +class SVGParseCompleteListener; + +class VectorImage final : public ImageResource, public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_IMGICONTAINER + + // (no public constructor - use ImageFactory) + + // Methods inherited from Image + virtual size_t SizeOfSourceWithComputedFallback( + SizeOfState& aState) const override; + + virtual nsresult OnImageDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStr, + uint64_t aSourceOffset, + uint32_t aCount) override; + virtual nsresult OnImageDataComplete(nsIRequest* aRequest, nsresult aResult, + bool aLastPart) override; + + virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override; + + /** + * Callback for SVGRootRenderingObserver. + * + * This just sets a dirty flag that we check in VectorImage::RequestRefresh, + * which is called under the ticks of the refresh driver of any observing + * documents that we may have. Only then (after all animations in this image + * have been updated) do we send out "frame changed" notifications, + */ + void InvalidateObserversOnNextRefreshDriverTick(); + + // Callback for SVGParseCompleteListener. + void OnSVGDocumentParsed(); + + // Callbacks for SVGLoadEventListener. + void OnSVGDocumentLoaded(); + void OnSVGDocumentError(); + + protected: + explicit VectorImage(nsIURI* aURI = nullptr); + virtual ~VectorImage(); + + virtual nsresult StartAnimation() override; + virtual nsresult StopAnimation() override; + virtual bool ShouldAnimate() override; + + private: + friend class SourceSurfaceBlobImage; + + /** + * Attempt to find a matching cached surface in the SurfaceCache. Returns the + * cached surface, if found, and the size to rasterize at, if applicable. + * If we cannot rasterize, it will be the requested size to draw at (aSize). + */ + std::tuple, gfx::IntSize> LookupCachedSurface( + const gfx::IntSize& aSize, const SVGImageContext& aSVGContext, + uint32_t aFlags); + + bool MaybeRestrictSVGContext(SVGImageContext& aSVGContext, uint32_t aFlags); + + /// Create a gfxDrawable which callbacks into the SVG document. + already_AddRefed CreateSVGDrawable( + const SVGDrawingParameters& aParams); + + /// Rasterize the SVG into a surface. aWillCache will be set to whether or + /// not the new surface was put into the cache. + already_AddRefed CreateSurface( + const SVGDrawingParameters& aParams, gfxDrawable* aSVGDrawable, + bool& aWillCache); + + /// Send a frame complete notification if appropriate. Must be called only + /// after all drawing has been completed. + void SendFrameComplete(bool aDidCache, uint32_t aFlags); + + void Show(gfxDrawable* aDrawable, const SVGDrawingParameters& aParams); + + nsresult Init(const char* aMimeType, uint32_t aFlags); + + /** + * In catastrophic circumstances like a GPU driver crash, we may lose our + * surfaces even if they're locked. RecoverFromLossOfSurfaces discards all + * existing surfaces, allowing us to recover. + */ + void RecoverFromLossOfSurfaces(); + + void CancelAllListeners(); + void SendInvalidationNotifications(); + + void ReportDocumentUseCounters(); + + RefPtr mSVGDocumentWrapper; + RefPtr mRenderingObserver; + RefPtr mLoadEventListener; + RefPtr mParseCompleteListener; + + /// Count of locks on this image (roughly correlated to visible instances). + uint32_t mLockCount; + + // Stored result from the Necko load of the image, which we save in + // OnImageDataComplete if the underlying SVG document isn't loaded. If we save + // this, we actually notify this progress (and clear this value) in + // OnSVGDocumentLoaded or OnSVGDocumentError. + Maybe mLoadProgress; + + bool mIsInitialized; // Have we been initialized? + bool mDiscardable; // Are we discardable? + bool mIsFullyLoaded; // Has the SVG document finished + // loading? + bool mHaveAnimations; // Is our SVG content SMIL-animated? + // (Only set after mIsFullyLoaded.) + bool mHasPendingInvalidation; // Invalidate observers next refresh + // driver tick. + + friend class ImageFactory; +}; + +inline NS_IMETHODIMP VectorImage::GetAnimationMode(uint16_t* aAnimationMode) { + return GetAnimationModeInternal(aAnimationMode); +} + +inline NS_IMETHODIMP VectorImage::SetAnimationMode(uint16_t aAnimationMode) { + return SetAnimationModeInternal(aAnimationMode); +} + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_VectorImage_h diff --git a/image/WebRenderImageProvider.h b/image/WebRenderImageProvider.h new file mode 100644 index 0000000000..84fee8cb47 --- /dev/null +++ b/image/WebRenderImageProvider.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_IMAGE_WEBRENDERIMAGEPROVIDER_H_ +#define MOZILLA_IMAGE_WEBRENDERIMAGEPROVIDER_H_ + +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace layers { +class RenderRootStateManager; +} + +namespace wr { +class IpcResourceUpdateQueue; +struct ExternalImageId; +struct ImageKey; +} // namespace wr + +namespace image { + +class ImageResource; +using ImageProviderId = uint32_t; + +class WebRenderImageProvider { + public: + // Subclasses may or may not be XPCOM classes, so we just require that they + // implement AddRef and Release. + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + ImageProviderId GetProviderId() const { return mProviderId; } + + static ImageProviderId AllocateProviderId(); + + /** + * Generate an ImageKey for the given frame. + * @param aSurface The current frame. This should match what was cached via + * SetCurrentFrame, but if it does not, it will need to + * regenerate the cached ImageKey. + */ + virtual nsresult UpdateKey(layers::RenderRootStateManager* aManager, + wr::IpcResourceUpdateQueue& aResources, + wr::ImageKey& aKey) { + return NS_ERROR_NOT_AVAILABLE; + } + + /** + * Invalidate if a blob recording, requiring it to be regenerated. + */ + virtual void InvalidateRecording() {} + + protected: + WebRenderImageProvider(const ImageResource* aImage); + + private: + ImageProviderId mProviderId; +}; + +} // namespace image +} // namespace mozilla + +#endif /* MOZILLA_IMAGE_WEBRENDERIMAGEPROVIDER_H_ */ diff --git a/image/build/components.conf b/image/build/components.conf new file mode 100644 index 0000000000..182d1cd1af --- /dev/null +++ b/image/build/components.conf @@ -0,0 +1,85 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Headers = [ + '/image/build/nsImageModule.h', +] + +InitFunc = 'mozilla::image::EnsureModuleInitialized' +# This is called by LayoutModuleDtor to ensure it happens at the right time +# during shutdown. +# UnloadFunc = 'mozilla::image::ShutdownModule' + +Classes = [ + # XXX We would like to get rid of the imgLoader factory constructor. See the + # comment documenting the imgLoader constructor. + { + 'cid': '{c1354898-e3fe-4602-88a7-c4520c21cb4e}', + 'contract_ids': [ + '@mozilla.org/image/cache;1', + '@mozilla.org/image/loader;1', + ], + 'type': 'imgLoader', + 'headers': ['imgLoader.h'], + 'init_method': 'Init', + 'categories': { + 'content-sniffing-services': '@mozilla.org/image/loader;1', + 'orb-content-sniffers': '@mozilla.org/image/loader;1', + 'net-and-orb-content-sniffers': '@mozilla.org/image/loader;1', + }, + }, + { + 'cid': '{f6fcd651-164b-4416-b001-9c8c393fd93b}', + 'contract_ids': [ + '@mozilla.org/image-blocker-content-policy;1', + ], + 'type': 'mozilla::image::ImageBlocker', + 'headers': ['ImageBlocker.h'], + 'categories': {'content-policy': '@mozilla.org/image-blocker-content-policy;1'}, + }, + { + 'cid': '{20557898-1dd2-11b2-8f65-9c462ee2bc95}', + 'contract_ids': ['@mozilla.org/image/request;1'], + 'type': 'imgRequestProxy', + 'headers': ['imgRequestProxy.h'], + }, + { + 'cid': '{3d8fa16d-c9e1-4b50-bdef-2c7ae249967a}', + 'contract_ids': ['@mozilla.org/image/tools;1'], + 'type': 'mozilla::image::imgTools', + 'headers': ['/image/imgTools.h'], + }, + { + 'cid': '{13a5320c-4c91-4fa4-bd16-b081a3ba8c0b}', + 'contract_ids': ['@mozilla.org/image/encoder;2?type=image/bmp'], + 'type': 'nsBMPEncoder', + 'headers': ['/image/encoders/bmp/nsBMPEncoder.h'], + }, + { + 'cid': '{92ae3ab2-8968-41b1-8709-b6123bceaf21}', + 'contract_ids': ['@mozilla.org/image/encoder;2?type=image/vnd.microsoft.icon'], + 'type': 'nsICOEncoder', + 'headers': ['/image/encoders/ico/nsICOEncoder.h'], + }, + { + 'cid': '{ac2bb8fe-eeeb-4572-b40f-be03932b56e0}', + 'contract_ids': ['@mozilla.org/image/encoder;2?type=image/jpeg'], + 'type': 'nsJPEGEncoder', + 'headers': ['/image/encoders/jpeg/nsJPEGEncoder.h'], + }, + { + 'cid': '{38d1592e-b81e-432b-86f8-471878bbfe07}', + 'contract_ids': ['@mozilla.org/image/encoder;2?type=image/png'], + 'type': 'nsPNGEncoder', + 'headers': ['/image/encoders/png/nsPNGEncoder.h'], + }, + { + 'cid': '{a8e5a8e5-bebf-4512-9f50-e41e4748ce28}', + 'contract_ids': ['@mozilla.org/image/encoder;2?type=image/webp'], + 'type': 'nsWebPEncoder', + 'headers': ['/image/encoders/webp/nsWebPEncoder.h'], + }, +] diff --git a/image/build/moz.build b/image/build/moz.build new file mode 100644 index 0000000000..7ba10e42b9 --- /dev/null +++ b/image/build/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + "nsImageModule.h", +] + +SOURCES += [ + "nsImageModule.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/image", + "/image/encoders/bmp", + "/image/encoders/ico", + "/image/encoders/jpeg", + "/image/encoders/png", +] diff --git a/image/build/nsImageModule.cpp b/image/build/nsImageModule.cpp new file mode 100644 index 0000000000..28fb8bb330 --- /dev/null +++ b/image/build/nsImageModule.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsImageModule.h" + +#include "mozilla/ModuleUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_image.h" + +#include "DecodePool.h" +#include "ImageFactory.h" +#include "nsICategoryManager.h" +#include "nsServiceManagerUtils.h" +#include "ShutdownTracker.h" +#include "SurfaceCache.h" +#include "imgLoader.h" + +using namespace mozilla::image; + +struct ImageEnablementCookie { + bool (*mIsEnabled)(); + const nsLiteralCString mMimeType; +}; + +static void UpdateContentViewerRegistration(const char* aPref, void* aData) { + auto* cookie = static_cast(aData); + + nsCOMPtr catMan = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + if (!catMan) { + return; + } + + static nsLiteralCString kCategory = "Gecko-Content-Viewers"_ns; + static nsLiteralCString kContractId = + "@mozilla.org/content/plugin/document-loader-factory;1"_ns; + + if (cookie->mIsEnabled()) { + catMan->AddCategoryEntry(kCategory, cookie->mMimeType, kContractId, + false /* aPersist */, true /* aReplace */); + } else { + catMan->DeleteCategoryEntry( + kCategory, cookie->mMimeType, false /* aPersist */ + ); + } +} + +static bool sInitialized = false; +nsresult mozilla::image::EnsureModuleInitialized() { + MOZ_ASSERT(NS_IsMainThread()); + + if (sInitialized) { + return NS_OK; + } + + static ImageEnablementCookie kAVIFCookie = { + mozilla::StaticPrefs::image_avif_enabled, "image/avif"_ns}; + static ImageEnablementCookie kJXLCookie = { + mozilla::StaticPrefs::image_jxl_enabled, "image/jxl"_ns}; + static ImageEnablementCookie kWebPCookie = { + mozilla::StaticPrefs::image_webp_enabled, "image/webp"_ns}; + Preferences::RegisterCallbackAndCall(UpdateContentViewerRegistration, + "image.avif.enabled", &kAVIFCookie); + Preferences::RegisterCallbackAndCall(UpdateContentViewerRegistration, + "image.jxl.enabled", &kJXLCookie); + Preferences::RegisterCallbackAndCall(UpdateContentViewerRegistration, + "image.webp.enabled", &kWebPCookie); + + mozilla::image::ShutdownTracker::Initialize(); + mozilla::image::ImageFactory::Initialize(); + mozilla::image::DecodePool::Initialize(); + mozilla::image::SurfaceCache::Initialize(); + imgLoader::GlobalInit(); + sInitialized = true; + return NS_OK; +} + +void mozilla::image::ShutdownModule() { + if (!sInitialized) { + return; + } + imgLoader::Shutdown(); + mozilla::image::SurfaceCache::Shutdown(); + sInitialized = false; +} diff --git a/image/build/nsImageModule.h b/image/build/nsImageModule.h new file mode 100644 index 0000000000..130e59b868 --- /dev/null +++ b/image/build/nsImageModule.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_build_nsImageModule_h +#define mozilla_image_build_nsImageModule_h + +#include "nsError.h" + +namespace mozilla { +namespace image { + +nsresult EnsureModuleInitialized(); +void ShutdownModule(); + +} /* namespace image */ +} /* namespace mozilla */ + +#endif // mozilla_image_build_nsImageModule_h diff --git a/image/decoders/EXIF.cpp b/image/decoders/EXIF.cpp new file mode 100644 index 0000000000..97563248c7 --- /dev/null +++ b/image/decoders/EXIF.cpp @@ -0,0 +1,519 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EXIF.h" + +#include "mozilla/EndianUtils.h" +#include "mozilla/StaticPrefs_image.h" + +namespace mozilla::image { + +// Section references in this file refer to the EXIF v2.3 standard, also known +// as CIPA DC-008-Translation-2010. + +// See Section 4.6.4, Table 4. +// Typesafe enums are intentionally not used here since we're comparing to raw +// integers produced by parsing. +enum class EXIFTag : uint16_t { + Orientation = 0x112, + XResolution = 0x11a, + YResolution = 0x11b, + PixelXDimension = 0xa002, + PixelYDimension = 0xa003, + ResolutionUnit = 0x128, + IFDPointer = 0x8769, +}; + +// See Section 4.6.2. +enum EXIFType { + ByteType = 1, + ASCIIType = 2, + ShortType = 3, + LongType = 4, + RationalType = 5, + UndefinedType = 7, + SignedLongType = 9, + SignedRational = 10, +}; + +static const char* EXIFHeader = "Exif\0\0"; +static const uint32_t EXIFHeaderLength = 6; +static const uint32_t TIFFHeaderStart = EXIFHeaderLength; + +struct ParsedEXIFData { + Orientation orientation; + Maybe resolutionX; + Maybe resolutionY; + Maybe pixelXDimension; + Maybe pixelYDimension; + Maybe resolutionUnit; +}; + +static float ToDppx(float aResolution, ResolutionUnit aUnit) { + constexpr float kPointsPerInch = 72.0f; + constexpr float kPointsPerCm = 1.0f / 2.54f; + switch (aUnit) { + case ResolutionUnit::Dpi: + return aResolution / kPointsPerInch; + case ResolutionUnit::Dpcm: + return aResolution / kPointsPerCm; + } + MOZ_CRASH("Unknown resolution unit?"); +} + +static Resolution ResolutionFromParsedData(const ParsedEXIFData& aData, + const gfx::IntSize& aRealImageSize) { + if (!aData.resolutionUnit || !aData.resolutionX || !aData.resolutionY) { + return {}; + } + + Resolution resolution{ToDppx(*aData.resolutionX, *aData.resolutionUnit), + ToDppx(*aData.resolutionY, *aData.resolutionUnit)}; + + if (StaticPrefs::image_exif_density_correction_sanity_check_enabled()) { + if (!aData.pixelXDimension || !aData.pixelYDimension) { + return {}; + } + + const gfx::IntSize exifSize(*aData.pixelXDimension, *aData.pixelYDimension); + + gfx::IntSize scaledSize = aRealImageSize; + resolution.ApplyTo(scaledSize.width, scaledSize.height); + + if (exifSize != scaledSize) { + return {}; + } + } + + return resolution; +} + +///////////////////////////////////////////////////////////// +// Parse EXIF data, typically found in a JPEG's APP1 segment. +///////////////////////////////////////////////////////////// +EXIFData EXIFParser::ParseEXIF(const uint8_t* aData, const uint32_t aLength, + const gfx::IntSize& aRealImageSize) { + if (!Initialize(aData, aLength)) { + return EXIFData(); + } + + if (!ParseEXIFHeader()) { + return EXIFData(); + } + + uint32_t offsetIFD; + if (!ParseTIFFHeader(offsetIFD)) { + return EXIFData(); + } + + JumpTo(offsetIFD); + + ParsedEXIFData data; + ParseIFD(data); + + return EXIFData{data.orientation, + ResolutionFromParsedData(data, aRealImageSize)}; +} + +///////////////////////////////////////////////////////// +// Parse the EXIF header. (Section 4.7.2, Figure 30) +///////////////////////////////////////////////////////// +bool EXIFParser::ParseEXIFHeader() { + return MatchString(EXIFHeader, EXIFHeaderLength); +} + +///////////////////////////////////////////////////////// +// Parse the TIFF header. (Section 4.5.2, Table 1) +///////////////////////////////////////////////////////// +bool EXIFParser::ParseTIFFHeader(uint32_t& aIFD0OffsetOut) { + // Determine byte order. + if (MatchString("MM\0*", 4)) { + mByteOrder = ByteOrder::BigEndian; + } else if (MatchString("II*\0", 4)) { + mByteOrder = ByteOrder::LittleEndian; + } else { + return false; + } + + // Determine offset of the 0th IFD. (It shouldn't be greater than 64k, which + // is the maximum size of the entry APP1 segment.) + uint32_t ifd0Offset; + if (!ReadUInt32(ifd0Offset) || ifd0Offset > 64 * 1024) { + return false; + } + + // The IFD offset is relative to the beginning of the TIFF header, which + // begins after the EXIF header, so we need to increase the offset + // appropriately. + aIFD0OffsetOut = ifd0Offset + TIFFHeaderStart; + return true; +} + +// An arbitrary limit on the amount of pointers that we'll chase, to prevent bad +// inputs getting us stuck. +constexpr uint32_t kMaxEXIFDepth = 16; + +///////////////////////////////////////////////////////// +// Parse the entries in IFD0. (Section 4.6.2) +///////////////////////////////////////////////////////// +void EXIFParser::ParseIFD(ParsedEXIFData& aData, uint32_t aDepth) { + if (NS_WARN_IF(aDepth > kMaxEXIFDepth)) { + return; + } + + uint16_t entryCount; + if (!ReadUInt16(entryCount)) { + return; + } + + for (uint16_t entry = 0; entry < entryCount; ++entry) { + // Read the fields of the 12-byte entry. + uint16_t tag; + if (!ReadUInt16(tag)) { + return; + } + + uint16_t type; + if (!ReadUInt16(type)) { + return; + } + + uint32_t count; + if (!ReadUInt32(count)) { + return; + } + + switch (EXIFTag(tag)) { + case EXIFTag::Orientation: + // We should have an orientation value here; go ahead and parse it. + if (!ParseOrientation(type, count, aData.orientation)) { + return; + } + break; + case EXIFTag::ResolutionUnit: + if (!ParseResolutionUnit(type, count, aData.resolutionUnit)) { + return; + } + break; + case EXIFTag::XResolution: + if (!ParseResolution(type, count, aData.resolutionX)) { + return; + } + break; + case EXIFTag::YResolution: + if (!ParseResolution(type, count, aData.resolutionY)) { + return; + } + break; + case EXIFTag::PixelXDimension: + if (!ParseDimension(type, count, aData.pixelXDimension)) { + return; + } + break; + case EXIFTag::PixelYDimension: + if (!ParseDimension(type, count, aData.pixelYDimension)) { + return; + } + break; + case EXIFTag::IFDPointer: { + uint32_t offset; + if (!ReadUInt32(offset)) { + return; + } + + ScopedJump jump(*this, offset + TIFFHeaderStart); + ParseIFD(aData, aDepth + 1); + break; + } + + default: + Advance(4); + break; + } + } +} + +bool EXIFParser::ReadRational(float& aOut) { + // Values larger than 4 bytes (like rationals) are specified as an offset into + // the TIFF header. + uint32_t valueOffset; + if (!ReadUInt32(valueOffset)) { + return false; + } + ScopedJump jumpToHeader(*this, valueOffset + TIFFHeaderStart); + uint32_t numerator; + if (!ReadUInt32(numerator)) { + return false; + } + uint32_t denominator; + if (!ReadUInt32(denominator)) { + return false; + } + if (denominator == 0) { + return false; + } + aOut = float(numerator) / float(denominator); + return true; +} + +bool EXIFParser::ParseResolution(uint16_t aType, uint32_t aCount, + Maybe& aOut) { + if (!StaticPrefs::image_exif_density_correction_enabled()) { + Advance(4); + return true; + } + if (aType != RationalType || aCount != 1) { + return false; + } + float value; + if (!ReadRational(value)) { + return false; + } + if (value == 0.0f) { + return false; + } + aOut = Some(value); + return true; +} + +bool EXIFParser::ParseDimension(uint16_t aType, uint32_t aCount, + Maybe& aOut) { + if (!StaticPrefs::image_exif_density_correction_enabled()) { + Advance(4); + return true; + } + + if (aCount != 1) { + return false; + } + + switch (aType) { + case ShortType: { + uint16_t value; + if (!ReadUInt16(value)) { + return false; + } + aOut = Some(value); + Advance(2); + break; + } + case LongType: { + uint32_t value; + if (!ReadUInt32(value)) { + return false; + } + aOut = Some(value); + break; + } + default: + return false; + } + return true; +} + +bool EXIFParser::ParseResolutionUnit(uint16_t aType, uint32_t aCount, + Maybe& aOut) { + if (!StaticPrefs::image_exif_density_correction_enabled()) { + Advance(4); + return true; + } + if (aType != ShortType || aCount != 1) { + return false; + } + uint16_t value; + if (!ReadUInt16(value)) { + return false; + } + switch (value) { + case 2: + aOut = Some(ResolutionUnit::Dpi); + break; + case 3: + aOut = Some(ResolutionUnit::Dpcm); + break; + default: + return false; + } + + // This is a 32-bit field, but the unit value only occupies the first 16 bits. + // We need to advance another 16 bits to consume the entire field. + Advance(2); + return true; +} + +bool EXIFParser::ParseOrientation(uint16_t aType, uint32_t aCount, + Orientation& aOut) { + // Sanity check the type and count. + if (aType != ShortType || aCount != 1) { + return false; + } + + uint16_t value; + if (!ReadUInt16(value)) { + return false; + } + + switch (value) { + case 1: + aOut = Orientation(Angle::D0, Flip::Unflipped); + break; + case 2: + aOut = Orientation(Angle::D0, Flip::Horizontal); + break; + case 3: + aOut = Orientation(Angle::D180, Flip::Unflipped); + break; + case 4: + aOut = Orientation(Angle::D180, Flip::Horizontal); + break; + case 5: + aOut = Orientation(Angle::D90, Flip::Horizontal); + break; + case 6: + aOut = Orientation(Angle::D90, Flip::Unflipped); + break; + case 7: + aOut = Orientation(Angle::D270, Flip::Horizontal); + break; + case 8: + aOut = Orientation(Angle::D270, Flip::Unflipped); + break; + default: + return false; + } + + // This is a 32-bit field, but the orientation value only occupies the first + // 16 bits. We need to advance another 16 bits to consume the entire field. + Advance(2); + return true; +} + +bool EXIFParser::Initialize(const uint8_t* aData, const uint32_t aLength) { + if (aData == nullptr) { + return false; + } + + // An APP1 segment larger than 64k violates the JPEG standard. + if (aLength > 64 * 1024) { + return false; + } + + mStart = mCurrent = aData; + mLength = mRemainingLength = aLength; + mByteOrder = ByteOrder::Unknown; + return true; +} + +void EXIFParser::Advance(const uint32_t aDistance) { + if (mRemainingLength >= aDistance) { + mCurrent += aDistance; + mRemainingLength -= aDistance; + } else { + mCurrent = mStart; + mRemainingLength = 0; + } +} + +void EXIFParser::JumpTo(const uint32_t aOffset) { + if (mLength >= aOffset) { + mCurrent = mStart + aOffset; + mRemainingLength = mLength - aOffset; + } else { + mCurrent = mStart; + mRemainingLength = 0; + } +} + +bool EXIFParser::MatchString(const char* aString, const uint32_t aLength) { + if (mRemainingLength < aLength) { + return false; + } + + for (uint32_t i = 0; i < aLength; ++i) { + if (mCurrent[i] != aString[i]) { + return false; + } + } + + Advance(aLength); + return true; +} + +bool EXIFParser::MatchUInt16(const uint16_t aValue) { + if (mRemainingLength < 2) { + return false; + } + + bool matched; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + matched = LittleEndian::readUint16(mCurrent) == aValue; + break; + case ByteOrder::BigEndian: + matched = BigEndian::readUint16(mCurrent) == aValue; + break; + default: + MOZ_ASSERT_UNREACHABLE("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(2); + } + + return matched; +} + +bool EXIFParser::ReadUInt16(uint16_t& aValue) { + if (mRemainingLength < 2) { + return false; + } + + bool matched = true; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + aValue = LittleEndian::readUint16(mCurrent); + break; + case ByteOrder::BigEndian: + aValue = BigEndian::readUint16(mCurrent); + break; + default: + MOZ_ASSERT_UNREACHABLE("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(2); + } + + return matched; +} + +bool EXIFParser::ReadUInt32(uint32_t& aValue) { + if (mRemainingLength < 4) { + return false; + } + + bool matched = true; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + aValue = LittleEndian::readUint32(mCurrent); + break; + case ByteOrder::BigEndian: + aValue = BigEndian::readUint32(mCurrent); + break; + default: + MOZ_ASSERT_UNREACHABLE("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(4); + } + + return matched; +} + +} // namespace mozilla::image diff --git a/image/decoders/EXIF.h b/image/decoders/EXIF.h new file mode 100644 index 0000000000..eb23f8d537 --- /dev/null +++ b/image/decoders/EXIF.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_EXIF_h +#define mozilla_image_decoders_EXIF_h + +#include +#include "nsDebug.h" + +#include "Orientation.h" +#include "mozilla/Maybe.h" +#include "mozilla/image/Resolution.h" +#include "mozilla/gfx/Point.h" + +namespace mozilla::image { + +enum class ByteOrder : uint8_t { Unknown, LittleEndian, BigEndian }; + +struct EXIFData { + const Orientation orientation = Orientation(); + const Resolution resolution = Resolution(); +}; + +struct ParsedEXIFData; + +enum class ResolutionUnit : uint8_t { + Dpi, + Dpcm, +}; + +class EXIFParser { + public: + static EXIFData Parse(const uint8_t* aData, const uint32_t aLength, + const gfx::IntSize& aRealImageSize) { + EXIFParser parser; + return parser.ParseEXIF(aData, aLength, aRealImageSize); + } + + private: + EXIFParser() + : mStart(nullptr), + mCurrent(nullptr), + mLength(0), + mRemainingLength(0), + mByteOrder(ByteOrder::Unknown) {} + + EXIFData ParseEXIF(const uint8_t* aData, const uint32_t aLength, + const gfx::IntSize& aRealImageSize); + bool ParseEXIFHeader(); + bool ParseTIFFHeader(uint32_t& aIFD0OffsetOut); + + void ParseIFD(ParsedEXIFData&, uint32_t aDepth = 0); + bool ParseOrientation(uint16_t aType, uint32_t aCount, Orientation&); + bool ParseResolution(uint16_t aType, uint32_t aCount, Maybe&); + bool ParseResolutionUnit(uint16_t aType, uint32_t aCount, + Maybe&); + bool ParseDimension(uint16_t aType, uint32_t aCount, Maybe&); + + bool Initialize(const uint8_t* aData, const uint32_t aLength); + void Advance(const uint32_t aDistance); + void JumpTo(const uint32_t aOffset); + + uint32_t CurrentOffset() const { return mCurrent - mStart; } + + class ScopedJump { + EXIFParser& mParser; + uint32_t mOldOffset; + + public: + ScopedJump(EXIFParser& aParser, uint32_t aOffset) + : mParser(aParser), mOldOffset(aParser.CurrentOffset()) { + mParser.JumpTo(aOffset); + } + + ~ScopedJump() { mParser.JumpTo(mOldOffset); } + }; + + bool MatchString(const char* aString, const uint32_t aLength); + bool MatchUInt16(const uint16_t aValue); + bool ReadUInt16(uint16_t& aOut); + bool ReadUInt32(uint32_t& aOut); + bool ReadRational(float& aOut); + + const uint8_t* mStart; + const uint8_t* mCurrent; + uint32_t mLength; + uint32_t mRemainingLength; + ByteOrder mByteOrder; +}; + +} // namespace mozilla::image + +#endif // mozilla_image_decoders_EXIF_h diff --git a/image/decoders/GIF2.h b/image/decoders/GIF2.h new file mode 100644 index 0000000000..c0c6bf0fde --- /dev/null +++ b/image/decoders/GIF2.h @@ -0,0 +1,67 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_GIF2_H +#define mozilla_image_decoders_GIF2_H + +#define MAX_LZW_BITS 12 +#define MAX_BITS 4097 // 2^MAX_LZW_BITS+1 +#define MAX_COLORS 256 +#define MIN_HOLD_SIZE 256 + +enum { GIF_TRAILER = 0x3B }; // ';' +enum { GIF_IMAGE_SEPARATOR = 0x2C }; // ',' +enum { GIF_EXTENSION_INTRODUCER = 0x21 }; // '!' +enum { GIF_GRAPHIC_CONTROL_LABEL = 0xF9 }; +enum { GIF_APPLICATION_EXTENSION_LABEL = 0xFF }; + +// A GIF decoder's state +typedef struct gif_struct { + // LZW decoder state machine + uint8_t* stackp; // Current stack pointer + int datasize; + int codesize; + int codemask; + int avail; // Index of next available slot in dictionary + int oldcode; + uint8_t firstchar; + int bits; // Number of unread bits in "datum" + int32_t datum; // 32-bit input buffer + + // Output state machine + int64_t pixels_remaining; // Pixels remaining to be output. + + // Parameters for image frame currently being decoded + int tpixel; // Index of transparent pixel + int32_t disposal_method; // Restore to background, leave in place, etc. + uint32_t* local_colormap; // Per-image colormap + uint32_t local_colormap_buffer_size; // Size of the buffer containing the + // local colormap. + int local_colormap_size; // Size of local colormap array. + uint32_t delay_time; // Display time, in milliseconds, + // for this image in a multi-image GIF + + // Global (multi-image) state + int version; // Either 89 for GIF89 or 87 for GIF87 + int32_t screen_width; // Logical screen width & height + int32_t screen_height; + uint8_t global_colormap_depth; // Depth of global colormap array + uint16_t global_colormap_count; // Number of colors in global colormap + int images_decoded; // Counts images for multi-part GIFs + int loop_count; // Netscape specific extension block to control + // the number of animation loops a GIF + // renders. + + bool is_transparent; // TRUE, if tpixel is valid + + uint16_t prefix[MAX_BITS]; // LZW decoding tables + uint32_t global_colormap[MAX_COLORS]; // Default colormap if local not + // supplied + uint8_t suffix[MAX_BITS]; // LZW decoding tables + uint8_t stack[MAX_BITS]; // Base of LZW decoder stack + +} gif_struct; + +#endif // mozilla_image_decoders_GIF2_H diff --git a/image/decoders/iccjpeg.c b/image/decoders/iccjpeg.c new file mode 100644 index 0000000000..6157fe8298 --- /dev/null +++ b/image/decoders/iccjpeg.c @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * iccjpeg.c + * + * This file provides code to read and write International Color Consortium + * (ICC) device profiles embedded in JFIF JPEG image files. The ICC has + * defined a standard format for including such data in JPEG "APP2" markers. + * The code given here does not know anything about the internal structure + * of the ICC profile data; it just knows how to put the profile data into + * a JPEG file being written, or get it back out when reading. + * + * This code depends on new features added to the IJG JPEG library as of + * IJG release 6b; it will not compile or work with older IJG versions. + * + * NOTE: this code would need surgery to work on 16-bit-int machines + * with ICC profiles exceeding 64K bytes in size. If you need to do that, + * change all the "unsigned int" variables to "INT32". You'll also need + * to find a malloc() replacement that can allocate more than 64K. + */ + +#include "iccjpeg.h" +#include /* define malloc() */ + +/* + * Since an ICC profile can be larger than the maximum size of a JPEG marker + * (64K), we need provisions to split it into multiple markers. The format + * defined by the ICC specifies one or more APP2 markers containing the + * following data: + * Identifying string ASCII "ICC_PROFILE\0" (12 bytes) + * Marker sequence number 1 for first APP2, 2 for next, etc (1 byte) + * Number of markers Total number of APP2's used (1 byte) + * Profile data (remainder of APP2 data) + * Decoders should use the marker sequence numbers to reassemble the profile, + * rather than assuming that the APP2 markers appear in the correct sequence. + */ + +#define ICC_MARKER (JPEG_APP0 + 2) /* JPEG marker code for ICC */ +#define ICC_OVERHEAD_LEN 14 /* size of non-profile data in APP2 */ +#define MAX_BYTES_IN_MARKER 65533 /* maximum data len of a JPEG marker */ +#define MAX_DATA_BYTES_IN_MARKER (MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN) + +/* + * Prepare for reading an ICC profile + */ + +void setup_read_icc_profile(j_decompress_ptr cinfo) { + /* Tell the library to keep any APP2 data it may find */ + jpeg_save_markers(cinfo, ICC_MARKER, 0xFFFF); +} + +/* + * Handy subroutine to test whether a saved marker is an ICC profile marker. + */ + +static boolean marker_is_icc(jpeg_saved_marker_ptr marker) { + return marker->marker == ICC_MARKER && + marker->data_length >= ICC_OVERHEAD_LEN && + /* verify the identifying string */ + GETJOCTET(marker->data[0]) == 0x49 && + GETJOCTET(marker->data[1]) == 0x43 && + GETJOCTET(marker->data[2]) == 0x43 && + GETJOCTET(marker->data[3]) == 0x5F && + GETJOCTET(marker->data[4]) == 0x50 && + GETJOCTET(marker->data[5]) == 0x52 && + GETJOCTET(marker->data[6]) == 0x4F && + GETJOCTET(marker->data[7]) == 0x46 && + GETJOCTET(marker->data[8]) == 0x49 && + GETJOCTET(marker->data[9]) == 0x4C && + GETJOCTET(marker->data[10]) == 0x45 && + GETJOCTET(marker->data[11]) == 0x0; +} + +/* + * See if there was an ICC profile in the JPEG file being read; + * if so, reassemble and return the profile data. + * + * TRUE is returned if an ICC profile was found, FALSE if not. + * If TRUE is returned, *icc_data_ptr is set to point to the + * returned data, and *icc_data_len is set to its length. + * + * IMPORTANT: the data at **icc_data_ptr has been allocated with malloc() + * and must be freed by the caller with free() when the caller no longer + * needs it. (Alternatively, we could write this routine to use the + * IJG library's memory allocator, so that the data would be freed implicitly + * at jpeg_finish_decompress() time. But it seems likely that many apps + * will prefer to have the data stick around after decompression finishes.) + * + * NOTE: if the file contains invalid ICC APP2 markers, we just silently + * return FALSE. You might want to issue an error message instead. + */ + +boolean read_icc_profile(j_decompress_ptr cinfo, JOCTET** icc_data_ptr, + unsigned int* icc_data_len) { + jpeg_saved_marker_ptr marker; + int num_markers = 0; + int seq_no; + JOCTET* icc_data; + unsigned int total_length; +#define MAX_SEQ_NO 255 /* sufficient since marker numbers are bytes */ + char marker_present[MAX_SEQ_NO + 1]; /* 1 if marker found */ + unsigned int data_length[MAX_SEQ_NO + 1]; /* size of profile data in marker */ + unsigned int data_offset[MAX_SEQ_NO + 1]; /* offset for data in marker */ + + *icc_data_ptr = NULL; /* avoid confusion if FALSE return */ + *icc_data_len = 0; + + /* This first pass over the saved markers discovers whether there are + * any ICC markers and verifies the consistency of the marker numbering. + */ + + for (seq_no = 1; seq_no <= MAX_SEQ_NO; seq_no++) { + marker_present[seq_no] = 0; + } + + for (marker = cinfo->marker_list; marker != NULL; marker = marker->next) { + if (marker_is_icc(marker)) { + if (num_markers == 0) { + num_markers = GETJOCTET(marker->data[13]); + } else if (num_markers != GETJOCTET(marker->data[13])) { + return FALSE; /* inconsistent num_markers fields */ + } + seq_no = GETJOCTET(marker->data[12]); + if (seq_no <= 0 || seq_no > num_markers) { + return FALSE; /* bogus sequence number */ + } + if (marker_present[seq_no]) { + return FALSE; /* duplicate sequence numbers */ + } + marker_present[seq_no] = 1; + data_length[seq_no] = marker->data_length - ICC_OVERHEAD_LEN; + } + } + + if (num_markers == 0) { + return FALSE; + } + + /* Check for missing markers, count total space needed, + * compute offset of each marker's part of the data. + */ + + total_length = 0; + for (seq_no = 1; seq_no <= num_markers; seq_no++) { + if (marker_present[seq_no] == 0) { + return FALSE; /* missing sequence number */ + } + data_offset[seq_no] = total_length; + total_length += data_length[seq_no]; + } + + if (total_length <= 0) { + return FALSE; /* found only empty markers? */ + } + + /* Allocate space for assembled data */ + icc_data = (JOCTET*)malloc(total_length * sizeof(JOCTET)); + if (icc_data == NULL) { + return FALSE; /* oops, out of memory */ + } + + /* and fill it in */ + for (marker = cinfo->marker_list; marker != NULL; marker = marker->next) { + if (marker_is_icc(marker)) { + JOCTET FAR* src_ptr; + JOCTET* dst_ptr; + unsigned int length; + seq_no = GETJOCTET(marker->data[12]); + dst_ptr = icc_data + data_offset[seq_no]; + src_ptr = marker->data + ICC_OVERHEAD_LEN; + length = data_length[seq_no]; + while (length--) { + *dst_ptr++ = *src_ptr++; + } + } + } + + *icc_data_ptr = icc_data; + *icc_data_len = total_length; + + return TRUE; +} diff --git a/image/decoders/iccjpeg.h b/image/decoders/iccjpeg.h new file mode 100644 index 0000000000..4d48144a23 --- /dev/null +++ b/image/decoders/iccjpeg.h @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * iccjpeg.h + * + * This file provides code to read and write International Color Consortium + * (ICC) device profiles embedded in JFIF JPEG image files. The ICC has + * defined a standard format for including such data in JPEG "APP2" markers. + * The code given here does not know anything about the internal structure + * of the ICC profile data; it just knows how to put the profile data into + * a JPEG file being written, or get it back out when reading. + * + * This code depends on new features added to the IJG JPEG library as of + * IJG release 6b; it will not compile or work with older IJG versions. + * + * NOTE: this code would need surgery to work on 16-bit-int machines + * with ICC profiles exceeding 64K bytes in size. See iccprofile.c + * for details. + */ + +#ifndef mozilla_image_decoders_iccjpeg_h +#define mozilla_image_decoders_iccjpeg_h + +#include /* needed to define "FILE", "NULL" */ +#include "jpeglib.h" + +/* + * Reading a JPEG file that may contain an ICC profile requires two steps: + * + * 1. After jpeg_create_decompress() but before jpeg_read_header(), + * call setup_read_icc_profile(). This routine tells the IJG library + * to save in memory any APP2 markers it may find in the file. + * + * 2. After jpeg_read_header(), call read_icc_profile() to find out + * whether there was a profile and obtain it if so. + */ + +/* + * Prepare for reading an ICC profile + */ + +extern void setup_read_icc_profile JPP((j_decompress_ptr cinfo)); + +/* + * See if there was an ICC profile in the JPEG file being read; + * if so, reassemble and return the profile data. + * + * TRUE is returned if an ICC profile was found, FALSE if not. + * If TRUE is returned, *icc_data_ptr is set to point to the + * returned data, and *icc_data_len is set to its length. + * + * IMPORTANT: the data at **icc_data_ptr has been allocated with malloc() + * and must be freed by the caller with free() when the caller no longer + * needs it. (Alternatively, we could write this routine to use the + * IJG library's memory allocator, so that the data would be freed implicitly + * at jpeg_finish_decompress() time. But it seems likely that many apps + * will prefer to have the data stick around after decompression finishes.) + */ + +extern boolean read_icc_profile JPP((j_decompress_ptr cinfo, + JOCTET** icc_data_ptr, + unsigned int* icc_data_len)); +#endif // mozilla_image_decoders_iccjpeg_h diff --git a/image/decoders/icon/android/moz.build b/image/decoders/icon/android/moz.build new file mode 100644 index 0000000000..a99ae228d4 --- /dev/null +++ b/image/decoders/icon/android/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsIconChannel.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/image/decoders/icon/android/nsIconChannel.cpp b/image/decoders/icon/android/nsIconChannel.cpp new file mode 100644 index 0000000000..7599cf2bb7 --- /dev/null +++ b/image/decoders/icon/android/nsIconChannel.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include "mozilla/gfx/Swizzle.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/NullPrincipal.h" +#include "nsMimeTypes.h" +#include "nsXULAppAPI.h" +#include "AndroidBridge.h" +#include "nsIconChannel.h" +#include "nsIIconURI.h" +#include "nsIStringStream.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsIconChannel, nsIRequest, nsIChannel) + +using namespace mozilla; +using mozilla::dom::ContentChild; + +static nsresult GetIconForExtension(const nsACString& aFileExt, + uint32_t aIconSize, uint8_t* const aBuf) { + if (!AndroidBridge::Bridge()) { + return NS_ERROR_FAILURE; + } + + AndroidBridge::Bridge()->GetIconForExtension(aFileExt, aIconSize, aBuf); + + return NS_OK; +} + +static nsresult CallRemoteGetIconForExtension(const nsACString& aFileExt, + uint32_t aIconSize, + uint8_t* const aBuf) { + NS_ENSURE_TRUE(aBuf != nullptr, NS_ERROR_NULL_POINTER); + + // An array has to be used to get data from remote process + nsTArray bits; + uint32_t bufSize = aIconSize * aIconSize * 4; + + if (!ContentChild::GetSingleton()->SendGetIconForExtension( + PromiseFlatCString(aFileExt), aIconSize, &bits)) { + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(bits.Length() == bufSize, "Pixels array is incomplete"); + if (bits.Length() != bufSize) { + return NS_ERROR_FAILURE; + } + + memcpy(aBuf, bits.Elements(), bufSize); + + return NS_OK; +} + +static nsresult moz_icon_to_channel(nsIURI* aURI, const nsACString& aFileExt, + uint32_t aIconSize, nsIChannel** aChannel) { + NS_ENSURE_TRUE(aIconSize < 256 && aIconSize > 0, NS_ERROR_UNEXPECTED); + + int width = aIconSize; + int height = aIconSize; + + // moz-icon data should have two bytes for the size, + // then the ARGB pixel values with pre-multiplied Alpha + const int channels = 4; + CheckedInt32 buf_size = + 4 + channels * CheckedInt32(height) * CheckedInt32(width); + if (!buf_size.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + uint8_t* const buf = (uint8_t*)moz_xmalloc(buf_size.value()); + uint8_t* out = buf; + + *(out++) = width; + *(out++) = height; + *(out++) = uint8_t(mozilla::gfx::SurfaceFormat::R8G8B8A8); + + // Set all bits to ensure in nsIconDecoder we color manage and premultiply. + *(out++) = 0xFF; + + nsresult rv; + if (XRE_IsParentProcess()) { + rv = GetIconForExtension(aFileExt, aIconSize, out); + } else { + rv = CallRemoteGetIconForExtension(aFileExt, aIconSize, out); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stream->AdoptData((char*)buf, buf_size.value()); + NS_ENSURE_SUCCESS(rv, rv); + + // nsIconProtocolHandler::NewChannel will provide the correct loadInfo for + // this iconChannel. Use the most restrictive security settings for the + // temporary loadInfo to make sure the channel can not be opened. + nsCOMPtr nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + return NS_NewInputStreamChannel( + aChannel, aURI, stream.forget(), nullPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, nsLiteralCString(IMAGE_ICON_MS)); +} + +nsresult nsIconChannel::Init(nsIURI* aURI) { + nsCOMPtr iconURI = do_QueryInterface(aURI); + NS_ASSERTION(iconURI, "URI is not an nsIMozIconURI"); + + nsAutoCString stockIcon; + iconURI->GetStockIcon(stockIcon); + + uint32_t desiredImageSize; + iconURI->GetImageSize(&desiredImageSize); + + nsAutoCString iconFileExt; + iconURI->GetFileExtension(iconFileExt); + + return moz_icon_to_channel(iconURI, iconFileExt, desiredImageSize, + getter_AddRefs(mRealChannel)); +} diff --git a/image/decoders/icon/android/nsIconChannel.h b/image/decoders/icon/android/nsIconChannel.h new file mode 100644 index 0000000000..e25196c6ee --- /dev/null +++ b/image/decoders/icon/android/nsIconChannel.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_icon_android_nsIconChannel_h +#define mozilla_image_decoders_icon_android_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" + +/** + * This class is the Android implementation of nsIconChannel. + * It asks Android for an icon, and creates a new channel for + * that file to which all calls will be proxied. + */ +class nsIconChannel final : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_NSIREQUEST(mRealChannel->) + NS_FORWARD_NSICHANNEL(mRealChannel->) + + nsIconChannel() {} + + /** + * Called by nsIconProtocolHandler after it creates this channel. + * Must be called before calling any other function on this object. + * If this method fails, no other function must be called on this object. + */ + nsresult Init(nsIURI* aURI); + + private: + ~nsIconChannel() {} + + /** + * The channel to the temp icon file (e.g. to /tmp/2qy9wjqw.html). + * Will always be non-null after a successful Init. + */ + nsCOMPtr mRealChannel; +}; + +#endif // mozilla_image_decoders_icon_android_nsIconChannel_h diff --git a/image/decoders/icon/components.conf b/image/decoders/icon/components.conf new file mode 100644 index 0000000000..68bff8e231 --- /dev/null +++ b/image/decoders/icon/components.conf @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{d0f9db12-249c-11d5-9905-001083010e9b}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=moz-icon'], + 'type': 'nsIconProtocolHandler', + 'headers': ['/image/decoders/icon/nsIconProtocolHandler.h'], + 'protocol_config': { + 'scheme': 'moz-icon', + 'flags': [ + 'URI_NORELATIVE', + 'URI_NOAUTH', + 'URI_IS_UI_RESOURCE', + 'URI_IS_LOCAL_RESOURCE', + ], + 'default_port': 0, + }, + }, + { + 'cid': '{1460df3b-774c-4205-8349-838e507c3ef9}', + 'type': 'nsMozIconURI::Mutator', + 'headers': ['/image/decoders/icon/nsIconURI.h'], + }, +] diff --git a/image/decoders/icon/gtk/moz.build b/image/decoders/icon/gtk/moz.build new file mode 100644 index 0000000000..3eac86b6ba --- /dev/null +++ b/image/decoders/icon/gtk/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsIconChannel.cpp", +] + +EXPORTS += [ + "nsIconChannel.h", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/image/decoders/icon/gtk/nsIconChannel.cpp b/image/decoders/icon/gtk/nsIconChannel.cpp new file mode 100644 index 0000000000..2b8b958b9e --- /dev/null +++ b/image/decoders/icon/gtk/nsIconChannel.cpp @@ -0,0 +1,483 @@ +/* vim:set ts=2 sw=2 sts=2 cin et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIconChannel.h" + +#include +#include + +#include "mozilla/DebugOnly.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/gfx/Swizzle.h" +#include "mozilla/ipc/ByteBuf.h" +#include + +#include + +#include + +#include "nsMimeTypes.h" +#include "nsIMIMEService.h" + +#include "nsServiceManagerUtils.h" + +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsIStringStream.h" +#include "nsServiceManagerUtils.h" +#include "nsIURL.h" +#include "nsIPipe.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "prlink.h" +#include "gfxPlatform.h" + +using mozilla::CheckedInt32; +using mozilla::ipc::ByteBuf; + +NS_IMPL_ISUPPORTS(nsIconChannel, nsIRequest, nsIChannel) + +static nsresult MozGdkPixbufToByteBuf(GdkPixbuf* aPixbuf, ByteBuf* aByteBuf) { + int width = gdk_pixbuf_get_width(aPixbuf); + int height = gdk_pixbuf_get_height(aPixbuf); + NS_ENSURE_TRUE(height < 256 && width < 256 && height > 0 && width > 0 && + gdk_pixbuf_get_colorspace(aPixbuf) == GDK_COLORSPACE_RGB && + gdk_pixbuf_get_bits_per_sample(aPixbuf) == 8 && + gdk_pixbuf_get_has_alpha(aPixbuf) && + gdk_pixbuf_get_n_channels(aPixbuf) == 4, + NS_ERROR_UNEXPECTED); + + const int n_channels = 4; + CheckedInt32 buf_size = + 4 + n_channels * CheckedInt32(height) * CheckedInt32(width); + if (!buf_size.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + uint8_t* const buf = (uint8_t*)moz_xmalloc(buf_size.value()); + uint8_t* out = buf; + + *(out++) = width; + *(out++) = height; + *(out++) = uint8_t(mozilla::gfx::SurfaceFormat::OS_RGBA); + + // Set all bits to ensure in nsIconDecoder we color manage and premultiply. + *(out++) = 0xFF; + + const guchar* const pixels = gdk_pixbuf_get_pixels(aPixbuf); + int instride = gdk_pixbuf_get_rowstride(aPixbuf); + int outstride = width * n_channels; + + // encode the RGB data and the A data and adjust the stride as necessary. + mozilla::gfx::SwizzleData(pixels, instride, + mozilla::gfx::SurfaceFormat::R8G8B8A8, out, + outstride, mozilla::gfx::SurfaceFormat::OS_RGBA, + mozilla::gfx::IntSize(width, height)); + + *aByteBuf = ByteBuf(buf, buf_size.value(), buf_size.value()); + return NS_OK; +} + +static nsresult ByteBufToStream(ByteBuf&& aBuf, nsIInputStream** aStream) { + nsresult rv; + nsCOMPtr stream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // stream takes ownership of buf and will free it on destruction. + // This function cannot fail. + rv = stream->AdoptData(reinterpret_cast(aBuf.mData), aBuf.mLen); + MOZ_ASSERT(CheckedInt32(aBuf.mLen).isValid(), + "aBuf.mLen should fit in int32_t"); + aBuf.mData = nullptr; + + // If this no longer holds then re-examine buf's lifetime. + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + + stream.forget(aStream); + return NS_OK; +} + +static nsresult StreamToChannel(already_AddRefed aStream, + nsIURI* aURI, nsIChannel** aChannel) { + // nsIconProtocolHandler::NewChannel will provide the correct loadInfo for + // this iconChannel. Use the most restrictive security settings for the + // temporary loadInfo to make sure the channel can not be opened. + nsCOMPtr nullPrincipal = + mozilla::NullPrincipal::CreateWithoutOriginAttributes(); + return NS_NewInputStreamChannel( + aChannel, aURI, std::move(aStream), nullPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, nsLiteralCString(IMAGE_ICON_MS)); +} + +static GtkWidget* gProtoWindow = nullptr; +static GtkWidget* gStockImageWidget = nullptr; + +static void ensure_stock_image_widget() { + // Only the style of the GtkImage needs to be used, but the widget is kept + // to track dynamic style changes. + if (!gProtoWindow) { + gProtoWindow = gtk_window_new(GTK_WINDOW_POPUP); + GtkWidget* protoLayout = gtk_fixed_new(); + gtk_container_add(GTK_CONTAINER(gProtoWindow), protoLayout); + + gStockImageWidget = gtk_image_new(); + gtk_container_add(GTK_CONTAINER(protoLayout), gStockImageWidget); + + gtk_widget_ensure_style(gStockImageWidget); + } +} + +static GtkIconSize moz_gtk_icon_size(const char* name) { + if (strcmp(name, "button") == 0) { + return GTK_ICON_SIZE_BUTTON; + } + + if (strcmp(name, "menu") == 0) { + return GTK_ICON_SIZE_MENU; + } + + if (strcmp(name, "toolbar") == 0) { + return GTK_ICON_SIZE_LARGE_TOOLBAR; + } + + if (strcmp(name, "toolbarsmall") == 0) { + return GTK_ICON_SIZE_SMALL_TOOLBAR; + } + + if (strcmp(name, "dnd") == 0) { + return GTK_ICON_SIZE_DND; + } + + if (strcmp(name, "dialog") == 0) { + return GTK_ICON_SIZE_DIALOG; + } + + return GTK_ICON_SIZE_MENU; +} + +static int32_t GetIconSize(nsIMozIconURI* aIconURI) { + nsAutoCString iconSizeString; + + aIconURI->GetIconSize(iconSizeString); + if (iconSizeString.IsEmpty()) { + uint32_t size; + mozilla::DebugOnly rv = aIconURI->GetImageSize(&size); + NS_ASSERTION(NS_SUCCEEDED(rv), "GetImageSize failed"); + return size; + } + int size; + + GtkIconSize icon_size = moz_gtk_icon_size(iconSizeString.get()); + gtk_icon_size_lookup(icon_size, &size, nullptr); + return size; +} + +/* Scale icon buffer to preferred size */ +static nsresult ScaleIconBuf(GdkPixbuf** aBuf, int32_t iconSize) { + // Scale buffer only if width or height differ from preferred size + if (gdk_pixbuf_get_width(*aBuf) != iconSize && + gdk_pixbuf_get_height(*aBuf) != iconSize) { + GdkPixbuf* scaled = + gdk_pixbuf_scale_simple(*aBuf, iconSize, iconSize, GDK_INTERP_BILINEAR); + // replace original buffer by scaled + g_object_unref(*aBuf); + *aBuf = scaled; + if (!scaled) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + return NS_OK; +} + +/* static */ +nsresult nsIconChannel::GetIconWithGIO(nsIMozIconURI* aIconURI, + ByteBuf* aDataOut) { + GIcon* icon = nullptr; + nsCOMPtr fileURI; + + // Read icon content + aIconURI->GetIconURL(getter_AddRefs(fileURI)); + + // Get icon for file specified by URI + if (fileURI) { + nsAutoCString spec; + fileURI->GetAsciiSpec(spec); + if (fileURI->SchemeIs("file")) { + GFile* file = g_file_new_for_uri(spec.get()); + GFileInfo* fileInfo = + g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_QUERY_INFO_NONE, nullptr, nullptr); + g_object_unref(file); + if (fileInfo) { + // icon from g_content_type_get_icon doesn't need unref + icon = g_file_info_get_icon(fileInfo); + if (icon) { + g_object_ref(icon); + } + g_object_unref(fileInfo); + } + } + } + + // Try to get icon by using MIME type + if (!icon) { + nsAutoCString type; + aIconURI->GetContentType(type); + // Try to get MIME type from file extension by using nsIMIMEService + if (type.IsEmpty()) { + nsCOMPtr ms(do_GetService("@mozilla.org/mime;1")); + if (ms) { + nsAutoCString fileExt; + aIconURI->GetFileExtension(fileExt); + ms->GetTypeFromExtension(fileExt, type); + } + } + char* ctype = nullptr; // character representation of content type + if (!type.IsEmpty()) { + ctype = g_content_type_from_mime_type(type.get()); + } + if (ctype) { + icon = g_content_type_get_icon(ctype); + g_free(ctype); + } + } + + // Get default icon theme + GtkIconTheme* iconTheme = gtk_icon_theme_get_default(); + GtkIconInfo* iconInfo = nullptr; + // Get icon size + int32_t iconSize = GetIconSize(aIconURI); + + if (icon) { + // Use icon and theme to get GtkIconInfo + iconInfo = gtk_icon_theme_lookup_by_gicon(iconTheme, icon, iconSize, + (GtkIconLookupFlags)0); + g_object_unref(icon); + } + + if (!iconInfo) { + // Mozilla's mimetype lookup failed. Try the "unknown" icon. + iconInfo = gtk_icon_theme_lookup_icon(iconTheme, "unknown", iconSize, + (GtkIconLookupFlags)0); + if (!iconInfo) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + // Create a GdkPixbuf buffer containing icon and scale it + GdkPixbuf* buf = gtk_icon_info_load_icon(iconInfo, nullptr); + gtk_icon_info_free(iconInfo); + if (!buf) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = ScaleIconBuf(&buf, iconSize); + NS_ENSURE_SUCCESS(rv, rv); + + rv = MozGdkPixbufToByteBuf(buf, aDataOut); + g_object_unref(buf); + return rv; +} + +/* static */ +nsresult nsIconChannel::GetIcon(nsIURI* aURI, ByteBuf* aDataOut) { + nsCOMPtr iconURI = do_QueryInterface(aURI); + NS_ASSERTION(iconURI, "URI is not an nsIMozIconURI"); + + if (!iconURI) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (gfxPlatform::IsHeadless()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString stockIcon; + iconURI->GetStockIcon(stockIcon); + if (stockIcon.IsEmpty()) { + return GetIconWithGIO(iconURI, aDataOut); + } + + // Search for stockIcon + nsAutoCString iconSizeString; + iconURI->GetIconSize(iconSizeString); + + nsAutoCString iconStateString; + iconURI->GetIconState(iconStateString); + + GtkIconSize icon_size = moz_gtk_icon_size(iconSizeString.get()); + GtkStateType state = iconStateString.EqualsLiteral("disabled") + ? GTK_STATE_INSENSITIVE + : GTK_STATE_NORMAL; + + // First lookup the icon by stock id and text direction. + GtkTextDirection direction = GTK_TEXT_DIR_NONE; + if (StringEndsWith(stockIcon, "-ltr"_ns)) { + direction = GTK_TEXT_DIR_LTR; + } else if (StringEndsWith(stockIcon, "-rtl"_ns)) { + direction = GTK_TEXT_DIR_RTL; + } + + bool forceDirection = direction != GTK_TEXT_DIR_NONE; + nsAutoCString stockID; + bool useIconName = false; + if (!forceDirection) { + direction = gtk_widget_get_default_direction(); + stockID = stockIcon; + } else { + // GTK versions < 2.22 use icon names from concatenating stock id with + // -(rtl|ltr), which is how the moz-icon stock name is interpreted here. + stockID = Substring(stockIcon, 0, stockIcon.Length() - 4); + // However, if we lookup bidi icons by the stock name, then GTK versions + // >= 2.22 will use a bidi lookup convention that most icon themes do not + // yet follow. Therefore, we first check to see if the theme supports the + // old icon name as this will have bidi support (if found). + GtkIconTheme* icon_theme = gtk_icon_theme_get_default(); + // Micking what gtk_icon_set_render_icon does with sizes, though it's not + // critical as icons will be scaled to suit size. It just means we follow + // the same paths and so share caches. + gint width, height; + if (gtk_icon_size_lookup(icon_size, &width, &height)) { + gint size = std::min(width, height); + // We use gtk_icon_theme_lookup_icon() without + // GTK_ICON_LOOKUP_USE_BUILTIN instead of gtk_icon_theme_has_icon() so + // we don't pick up fallback icons added by distributions for backward + // compatibility. + GtkIconInfo* icon = gtk_icon_theme_lookup_icon( + icon_theme, stockIcon.get(), size, (GtkIconLookupFlags)0); + if (icon) { + useIconName = true; + gtk_icon_info_free(icon); + } + } + } + + ensure_stock_image_widget(); + GtkStyle* style = gtk_widget_get_style(gStockImageWidget); + GtkIconSet* icon_set = nullptr; + if (!useIconName) { + icon_set = gtk_style_lookup_icon_set(style, stockID.get()); + } + + if (!icon_set) { + // Either we have chosen icon-name lookup for a bidi icon, or stockIcon is + // not a stock id so we assume it is an icon name. + useIconName = true; + // Creating a GtkIconSet is a convenient way to allow the style to + // render the icon, possibly with variations suitable for insensitive + // states. + icon_set = gtk_icon_set_new(); + GtkIconSource* icon_source = gtk_icon_source_new(); + + gtk_icon_source_set_icon_name(icon_source, stockIcon.get()); + gtk_icon_set_add_source(icon_set, icon_source); + gtk_icon_source_free(icon_source); + } + + GdkPixbuf* icon = gtk_icon_set_render_icon( + icon_set, style, direction, state, icon_size, gStockImageWidget, nullptr); + if (useIconName) { + gtk_icon_set_unref(icon_set); + } + + // According to documentation, gtk_icon_set_render_icon() never returns + // nullptr, but it does return nullptr when we have the problem reported + // here: https://bugzilla.gnome.org/show_bug.cgi?id=629878#c13 + if (!icon) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = MozGdkPixbufToByteBuf(icon, aDataOut); + + g_object_unref(icon); + + return rv; +} + +nsresult nsIconChannel::Init(nsIURI* aURI) { + nsCOMPtr stream; + + using ContentChild = mozilla::dom::ContentChild; + if (auto* contentChild = ContentChild::GetSingleton()) { + // Get the icon via IPC and translate the promise of a ByteBuf + // into an actually-existing channel. + RefPtr icon = + contentChild->SendGetSystemIcon(aURI); + if (!icon) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr inputStream; + nsCOMPtr outputStream; + NS_NewPipe2(getter_AddRefs(inputStream), getter_AddRefs(outputStream), true, + false, 0, UINT32_MAX); + + // FIXME: Bug 1718324 + // The GetSystemIcon() call will end up on the parent doing GetIcon() + // and by using ByteBuf we might not be immune to some deadlock, at least + // on paper. From analysis in + // https://phabricator.services.mozilla.com/D118596#3865440 we should be + // safe in practice, but it would be nicer to just write that differently. + + icon->Then( + mozilla::GetCurrentSerialEventTarget(), __func__, + [outputStream](std::tuple>&& aArg) { + nsresult rv = std::get<0>(aArg); + mozilla::Maybe bytes = std::move(std::get<1>(aArg)); + + if (NS_SUCCEEDED(rv)) { + MOZ_RELEASE_ASSERT(bytes); + uint32_t written; + rv = outputStream->Write(reinterpret_cast(bytes->mData), + static_cast(bytes->mLen), + &written); + if (NS_SUCCEEDED(rv)) { + const bool wroteAll = static_cast(written) == bytes->mLen; + MOZ_ASSERT(wroteAll); + if (!wroteAll) { + rv = NS_ERROR_UNEXPECTED; + } + } + } else { + MOZ_ASSERT(!bytes); + } + + if (NS_FAILED(rv)) { + outputStream->CloseWithStatus(rv); + } + }, + [outputStream](mozilla::ipc::ResponseRejectReason) { + outputStream->CloseWithStatus(NS_ERROR_FAILURE); + }); + + stream = inputStream.forget(); + } else { + // Get the icon directly. + ByteBuf bytebuf; + nsresult rv = GetIcon(aURI, &bytebuf); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ByteBufToStream(std::move(bytebuf), getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return StreamToChannel(stream.forget(), aURI, getter_AddRefs(mRealChannel)); +} + +void nsIconChannel::Shutdown() { + if (gProtoWindow) { + gtk_widget_destroy(gProtoWindow); + gProtoWindow = nullptr; + gStockImageWidget = nullptr; + } +} diff --git a/image/decoders/icon/gtk/nsIconChannel.h b/image/decoders/icon/gtk/nsIconChannel.h new file mode 100644 index 0000000000..6ad26602d0 --- /dev/null +++ b/image/decoders/icon/gtk/nsIconChannel.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_icon_gtk_nsIconChannel_h +#define mozilla_image_decoders_icon_gtk_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsIIconURI.h" +#include "nsCOMPtr.h" + +namespace mozilla::ipc { +class ByteBuf; +} + +/// This class is the GTK implementation of nsIconChannel. It asks +/// GTK for the icon, translates the pixel data in-memory into +/// nsIconDecoder format, and proxies the nsChannel interface to a new +/// channel returning that image. +class nsIconChannel final : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_NSIREQUEST(mRealChannel->) + NS_FORWARD_NSICHANNEL(mRealChannel->) + + nsIconChannel() {} + + static void Shutdown(); + + /// Called by nsIconProtocolHandler after it creates this channel. + /// Must be called before calling any other function on this object. + /// If this method fails, no other function must be called on this object. + nsresult Init(nsIURI* aURI); + + /// Obtains an icon, in nsIconDecoder format, as a ByteBuf instead + /// of a channel. For use with IPC. + static nsresult GetIcon(nsIURI* aURI, mozilla::ipc::ByteBuf* aDataOut); + + private: + ~nsIconChannel() {} + /// The input stream channel which will yield the image. + /// Will always be non-null after a successful Init. + nsCOMPtr mRealChannel; + + static nsresult GetIconWithGIO(nsIMozIconURI* aIconURI, + mozilla::ipc::ByteBuf* aDataOut); +}; + +#endif // mozilla_image_decoders_icon_gtk_nsIconChannel_h diff --git a/image/decoders/icon/mac/moz.build b/image/decoders/icon/mac/moz.build new file mode 100644 index 0000000000..3467659a8f --- /dev/null +++ b/image/decoders/icon/mac/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsIconChannelCocoa.mm", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/image/decoders/icon/mac/nsIconChannel.h b/image/decoders/icon/mac/nsIconChannel.h new file mode 100644 index 0000000000..dca2a3c51a --- /dev/null +++ b/image/decoders/icon/mac/nsIconChannel.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_icon_mac_nsIconChannel_h +#define mozilla_image_encoders_icon_mac_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIChannel.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +class nsIFile; + +class nsIconChannel final : public nsIChannel, public nsIStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsIconChannel(); + + nsresult Init(nsIURI* uri); + + protected: + virtual ~nsIconChannel(); + + nsCOMPtr mUrl; + nsCOMPtr mOriginalURI; + nsCOMPtr mLoadGroup; + nsCOMPtr mCallbacks; + nsCOMPtr mOwner; + nsCOMPtr mLoadInfo; + + nsCOMPtr mPump; + nsCOMPtr mListener; + bool mCanceled = false; + + [[nodiscard]] nsresult MakeInputStream(nsIInputStream** _retval, + bool nonBlocking); + + nsresult ExtractIconInfoFromUrl(nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, + nsACString& aContentType, + nsACString& aFileExtension); +}; + +#endif // mozilla_image_encoders_icon_mac_nsIconChannel_h diff --git a/image/decoders/icon/mac/nsIconChannelCocoa.mm b/image/decoders/icon/mac/nsIconChannelCocoa.mm new file mode 100644 index 0000000000..368ecdda20 --- /dev/null +++ b/image/decoders/icon/mac/nsIconChannelCocoa.mm @@ -0,0 +1,505 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsContentUtils.h" +#include "nsIconChannel.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/EndianUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIIconURI.h" +#include "nsIInputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsString.h" +#include "nsMimeTypes.h" +#include "nsIURL.h" +#include "nsNetCID.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsCExternalHandlerService.h" +#include "nsILocalFileMac.h" +#include "nsIFileURL.h" +#include "nsTArray.h" +#include "nsObjCExceptions.h" +#include "nsProxyRelease.h" +#include "nsContentSecurityManager.h" +#include "nsNetUtil.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtrExtensions.h" + +#include + +using namespace mozilla; + +// nsIconChannel methods +nsIconChannel::nsIconChannel() {} + +nsIconChannel::~nsIconChannel() { + if (mLoadInfo) { + NS_ReleaseOnMainThread("nsIconChannel::mLoadInfo", mLoadInfo.forget()); + } +} + +NS_IMPL_ISUPPORTS(nsIconChannel, nsIChannel, nsIRequest, nsIRequestObserver, nsIStreamListener) + +nsresult nsIconChannel::Init(nsIURI* uri) { + NS_ASSERTION(uri, "no uri"); + mUrl = uri; + mOriginalURI = uri; + nsresult rv; + mPump = do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +NS_IMETHODIMP +nsIconChannel::GetName(nsACString& result) { return mUrl->GetSpec(result); } + +NS_IMETHODIMP +nsIconChannel::IsPending(bool* result) { return mPump->IsPending(result); } + +NS_IMETHODIMP +nsIconChannel::GetStatus(nsresult* status) { return mPump->GetStatus(status); } + +NS_IMETHODIMP nsIconChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsIconChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsIconChannel::CancelWithReason(nsresult aStatus, const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +nsIconChannel::Cancel(nsresult status) { + mCanceled = true; + return mPump->Cancel(status); +} + +NS_IMETHODIMP +nsIconChannel::GetCanceled(bool* result) { + *result = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::Suspend(void) { return mPump->Suspend(); } + +NS_IMETHODIMP +nsIconChannel::Resume(void) { return mPump->Resume(); } + +// nsIRequestObserver methods +NS_IMETHODIMP +nsIconChannel::OnStartRequest(nsIRequest* aRequest) { + if (mListener) { + return mListener->OnStartRequest(this); + } + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + if (mListener) { + mListener->OnStopRequest(this, aStatus); + mListener = nullptr; + } + + // Remove from load group + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatus); + } + + return NS_OK; +} + +// nsIStreamListener methods +NS_IMETHODIMP +nsIconChannel::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, uint64_t aOffset, + uint32_t aCount) { + if (mListener) { + return mListener->OnDataAvailable(this, aStream, aOffset, aCount); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIChannel methods: + +NS_IMETHODIMP +nsIconChannel::GetOriginalURI(nsIURI** aURI) { + *aURI = mOriginalURI; + NS_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetURI(nsIURI** aURI) { + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::Open(nsIInputStream** _retval) { + nsCOMPtr listener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + return MakeInputStream(_retval, false); +} + +nsresult nsIconChannel::ExtractIconInfoFromUrl(nsIFile** aLocalFile, uint32_t* aDesiredImageSize, + nsACString& aContentType, + nsACString& aFileExtension) { + nsresult rv = NS_OK; + nsCOMPtr iconURI(do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + iconURI->GetImageSize(aDesiredImageSize); + iconURI->GetContentType(aContentType); + iconURI->GetFileExtension(aFileExtension); + + nsCOMPtr url; + rv = iconURI->GetIconURL(getter_AddRefs(url)); + if (NS_FAILED(rv) || !url) return NS_OK; + + nsCOMPtr fileURL = do_QueryInterface(url, &rv); + if (NS_FAILED(rv) || !fileURL) return NS_OK; + + nsCOMPtr file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) return NS_OK; + + nsCOMPtr localFileMac(do_QueryInterface(file, &rv)); + if (NS_FAILED(rv) || !localFileMac) return NS_OK; + + *aLocalFile = file; + NS_IF_ADDREF(*aLocalFile); + + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + MOZ_ASSERT(mLoadInfo->GetSecurityMode() == 0 || mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + nsCOMPtr inStream; + rv = MakeInputStream(getter_AddRefs(inStream), true); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + // Init our stream pump + nsCOMPtr target = + nsContentUtils::GetEventTargetByLoadInfo(mLoadInfo, mozilla::TaskCategory::Other); + rv = mPump->Init(inStream, 0, 0, false, target); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + rv = mPump->AsyncRead(this); + if (NS_SUCCEEDED(rv)) { + // Store our real listener + mListener = aListener; + // Add ourself to the load group, if available + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + } else { + mCallbacks = nullptr; + } + + return rv; +} + +nsresult nsIconChannel::MakeInputStream(nsIInputStream** _retval, bool aNonBlocking) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + nsCString contentType; + nsAutoCString fileExt; + nsCOMPtr fileloc; // file we want an icon for + uint32_t desiredImageSize; + nsresult rv = + ExtractIconInfoFromUrl(getter_AddRefs(fileloc), &desiredImageSize, contentType, fileExt); + NS_ENSURE_SUCCESS(rv, rv); + + bool fileExists = false; + if (fileloc) { + fileloc->Exists(&fileExists); + } + + NSImage* iconImage = nil; + + // first try to get the icon from the file if it exists + if (fileExists) { + nsCOMPtr localFileMac(do_QueryInterface(fileloc, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef macURL; + if (NS_SUCCEEDED(localFileMac->GetCFURL(&macURL))) { + iconImage = [[NSWorkspace sharedWorkspace] iconForFile:[(NSURL*)macURL path]]; + ::CFRelease(macURL); + } + } + + // if we don't have an icon yet try to get one by extension + if (!iconImage && !fileExt.IsEmpty()) { + NSString* fileExtension = [NSString stringWithUTF8String:fileExt.get()]; + iconImage = [[NSWorkspace sharedWorkspace] iconForFileType:fileExtension]; + } + + // If we still don't have an icon, get the generic document icon. + if (!iconImage) { + iconImage = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeUnknown]; + } + + if (!iconImage) { + return NS_ERROR_FAILURE; + } + + if (desiredImageSize > 255) { + // The Icon image format represents width and height as u8, so it does not + // allow for images sized 256 or more. + return NS_ERROR_FAILURE; + } + + // Set the actual size to *twice* the requested size. + // We do this because our UI doesn't take the window's device pixel ratio into + // account when it requests these icons; e.g. it will request an icon with + // size 16, place it in a 16x16 CSS pixel sized image, and then display it in + // a window on a HiDPI screen where the icon now covers 32x32 physical screen + // pixels. So we just always double the size here in order to prevent blurriness. + uint32_t size = (desiredImageSize < 128) ? desiredImageSize * 2 : desiredImageSize; + uint32_t width = size; + uint32_t height = size; + + // The "image format" we're outputting here (and which gets decoded by + // nsIconDecoder) has the following format: + // - 1 byte for the image width, as u8 + // - 1 byte for the image height, as u8 + // - the raw image data as BGRA, width * height * 4 bytes. + size_t bufferCapacity = 4 + width * height * 4; + UniquePtr fileBuf = MakeUniqueFallible(bufferCapacity); + if (NS_WARN_IF(!fileBuf)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + fileBuf[0] = uint8_t(width); + fileBuf[1] = uint8_t(height); + fileBuf[2] = uint8_t(mozilla::gfx::SurfaceFormat::B8G8R8A8); + + // Clear all bits to ensure in nsIconDecoder we assume we are already color + // managed and premultiplied. + fileBuf[3] = 0; + + uint8_t* imageBuf = &fileBuf[4]; + + // Create a CGBitmapContext around imageBuf and draw iconImage to it. + // This gives us the image data in the format we want: BGRA, four bytes per + // pixel, in host endianness, with premultiplied alpha. + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = + CGBitmapContextCreate(imageBuf, width, height, 8 /* bitsPerComponent */, width * 4, cs, + kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(cs); + + NSGraphicsContext* oldContext = [NSGraphicsContext currentContext]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:ctx + flipped:NO]]; + + [iconImage drawInRect:NSMakeRect(0, 0, width, height)]; + + [NSGraphicsContext setCurrentContext:oldContext]; + + CGContextRelease(ctx); + + // Now, create a pipe and stuff our data into it + nsCOMPtr inStream; + nsCOMPtr outStream; + NS_NewPipe(getter_AddRefs(inStream), getter_AddRefs(outStream), bufferCapacity, bufferCapacity, + aNonBlocking); + + uint32_t written; + rv = outStream->Write((char*)fileBuf.get(), bufferCapacity, &written); + if (NS_SUCCEEDED(rv)) { + NS_IF_ADDREF(*_retval = inStream); + } + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +NS_IMETHODIMP +nsIconChannel::GetLoadFlags(uint32_t* aLoadAttributes) { + return mPump->GetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::SetLoadFlags(uint32_t aLoadAttributes) { + return mPump->SetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { return GetTRRModeImpl(aTRRMode); } + +NS_IMETHODIMP +nsIconChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { return SetTRRModeImpl(aTRRMode); } + +NS_IMETHODIMP +nsIconChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +nsIconChannel::GetContentType(nsACString& aContentType) { + aContentType.AssignLiteral(IMAGE_ICON_MS); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentType(const nsACString& aContentType) { + // It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentCharset(nsACString& aContentCharset) { + aContentCharset.AssignLiteral(IMAGE_ICON_MS); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentCharset(const nsACString& aContentCharset) { + // It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDispositionFilename(nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDispositionFilename(const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDispositionHeader(nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = 0; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentLength(int64_t aContentLength) { + MOZ_ASSERT_UNREACHABLE("nsIconChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = mLoadGroup; + NS_IF_ADDREF(*aLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetOwner(nsISupports** aOwner) { + *aOwner = mOwner.get(); + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOwner(nsISupports* aOwner) { + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aNotificationCallbacks) { + *aNotificationCallbacks = mCallbacks.get(); + NS_IF_ADDREF(*aNotificationCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aNotificationCallbacks) { + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} diff --git a/image/decoders/icon/moz.build b/image/decoders/icon/moz.build new file mode 100644 index 0000000000..96cf951b3b --- /dev/null +++ b/image/decoders/icon/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "nsIconProtocolHandler.cpp", + "nsIconURI.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") + +EXPORTS += [ + "nsIconURI.h", +] + +platform = None + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + platform = "gtk" + +if CONFIG["OS_ARCH"] == "WINNT": + platform = "win" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + platform = "mac" + +if CONFIG["OS_TARGET"] == "Android": + platform = "android" + +if platform: + LOCAL_INCLUDES += [platform] diff --git a/image/decoders/icon/nsIconProtocolHandler.cpp b/image/decoders/icon/nsIconProtocolHandler.cpp new file mode 100644 index 0000000000..9334f908db --- /dev/null +++ b/image/decoders/icon/nsIconProtocolHandler.cpp @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIconProtocolHandler.h" + +#include "nsIconChannel.h" +#include "nsIconURI.h" +#include "nsCRT.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" + +/////////////////////////////////////////////////////////////////////////////// + +nsIconProtocolHandler::nsIconProtocolHandler() {} + +nsIconProtocolHandler::~nsIconProtocolHandler() {} + +NS_IMPL_ISUPPORTS(nsIconProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +/////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsIconProtocolHandler::GetScheme(nsACString& result) { + result = "moz-icon"; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::NewChannel(nsIURI* url, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(url); + nsIconChannel* channel = new nsIconChannel; + if (!channel) { + return NS_ERROR_OUT_OF_MEMORY; + } + NS_ADDREF(channel); + + nsresult rv = channel->Init(url); + if (NS_FAILED(rv)) { + NS_RELEASE(channel); + return rv; + } + + // set the loadInfo on the new channel + rv = channel->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + NS_RELEASE(channel); + return rv; + } + + *result = channel; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/image/decoders/icon/nsIconProtocolHandler.h b/image/decoders/icon/nsIconProtocolHandler.h new file mode 100644 index 0000000000..63843eaa4b --- /dev/null +++ b/image/decoders/icon/nsIconProtocolHandler.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_icon_nsIconProtocolHandler_h +#define mozilla_image_decoders_icon_nsIconProtocolHandler_h + +#include "nsWeakReference.h" +#include "nsIProtocolHandler.h" + +class nsIconProtocolHandler : public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + // nsIconProtocolHandler methods: + nsIconProtocolHandler(); + + protected: + virtual ~nsIconProtocolHandler(); +}; + +#endif // mozilla_image_decoders_icon_nsIconProtocolHandler_h diff --git a/image/decoders/icon/nsIconURI.cpp b/image/decoders/icon/nsIconURI.cpp new file mode 100644 index 0000000000..d917337bf9 --- /dev/null +++ b/image/decoders/icon/nsIconURI.cpp @@ -0,0 +1,654 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set sw=2 sts=2 ts=2 et tw=80: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIconURI.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Sprintf.h" + +#include "nsIClassInfoImpl.h" +#include "nsIIOService.h" +#include "nsISerializable.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "plstr.h" +#include "nsCRT.h" +#include + +using namespace mozilla; +using namespace mozilla::ipc; + +#define DEFAULT_IMAGE_SIZE 16 + +#if defined(MAX_PATH) +# define SANE_FILE_NAME_LEN MAX_PATH +#elif defined(PATH_MAX) +# define SANE_FILE_NAME_LEN PATH_MAX +#else +# define SANE_FILE_NAME_LEN 1024 +#endif + +static NS_DEFINE_CID(kThisIconURIImplementationCID, + NS_THIS_ICONURI_IMPLEMENTATION_CID); + +static const char* const kSizeStrings[] = {"button", "toolbar", "toolbarsmall", + "menu", "dnd", "dialog"}; + +static const char* const kStateStrings[] = {"normal", "disabled"}; + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_CLASSINFO(nsMozIconURI, nullptr, nsIClassInfo::THREADSAFE, + NS_ICONURI_CID) +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(nsMozIconURI) + +nsMozIconURI::nsMozIconURI() + : mSize(DEFAULT_IMAGE_SIZE), mIconSize(-1), mIconState(-1) {} + +nsMozIconURI::~nsMozIconURI() {} + +NS_IMPL_ADDREF(nsMozIconURI) +NS_IMPL_RELEASE(nsMozIconURI) + +NS_INTERFACE_MAP_BEGIN(nsMozIconURI) + if (aIID.Equals(kThisIconURIImplementationCID)) { + foundInterface = static_cast(this); + } else + NS_INTERFACE_MAP_ENTRY(nsIMozIconURI) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURI) + NS_INTERFACE_MAP_ENTRY(nsIURI) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsINestedURI, mIconURL) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + NS_IMPL_QUERY_CLASSINFO(nsMozIconURI) +NS_INTERFACE_MAP_END + +#define MOZICON_SCHEME "moz-icon:" +#define MOZICON_SCHEME_LEN (sizeof(MOZICON_SCHEME) - 1) + +//////////////////////////////////////////////////////////////////////////////// +// nsIURI methods: + +NS_IMETHODIMP +nsMozIconURI::GetSpec(nsACString& aSpec) { + aSpec = MOZICON_SCHEME; + + if (mIconURL) { + nsAutoCString fileIconSpec; + nsresult rv = mIconURL->GetSpec(fileIconSpec); + NS_ENSURE_SUCCESS(rv, rv); + aSpec += fileIconSpec; + } else if (!mStockIcon.IsEmpty()) { + aSpec += "//stock/"; + aSpec += mStockIcon; + } else { + aSpec += "//"; + aSpec += mFileName; + } + + aSpec += "?size="; + if (mIconSize >= 0) { + aSpec += kSizeStrings[mIconSize]; + } else { + char buf[20]; + SprintfLiteral(buf, "%d", mSize); + aSpec.Append(buf); + } + + if (mIconState >= 0) { + aSpec += "&state="; + aSpec += kStateStrings[mIconState]; + } + + if (!mContentType.IsEmpty()) { + aSpec += "&contentType="; + aSpec += mContentType.get(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetSpecIgnoringRef(nsACString& result) { return GetSpec(result); } + +NS_IMETHODIMP +nsMozIconURI::GetDisplaySpec(nsACString& aUnicodeSpec) { + return GetSpec(aUnicodeSpec); +} + +NS_IMETHODIMP +nsMozIconURI::GetDisplayHostPort(nsACString& aUnicodeHostPort) { + return GetHostPort(aUnicodeHostPort); +} + +NS_IMETHODIMP +nsMozIconURI::GetDisplayHost(nsACString& aUnicodeHost) { + return GetHost(aUnicodeHost); +} + +NS_IMETHODIMP +nsMozIconURI::GetDisplayPrePath(nsACString& aPrePath) { + return GetPrePath(aPrePath); +} + +NS_IMETHODIMP +nsMozIconURI::GetHasRef(bool* result) { + *result = false; + return NS_OK; +} + +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsMozIconURI::Mutator, nsIURISetters, + nsIURIMutator, nsISerializable) + +NS_IMETHODIMP +nsMozIconURI::Mutate(nsIURIMutator** aMutator) { + RefPtr mutator = new nsMozIconURI::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} + +// helper function for parsing out attributes like size, and contentType +// from the icon url. +// takes a string like ?size=32&contentType=text/html and returns a new string +// containing just the attribute value. i.e you could pass in this string with +// an attribute name of 'size=', this will return 32 +// Assumption: attribute pairs in the string are separated by '&'. +static void extractAttributeValue(const char* aSearchString, + const char* aAttributeName, + nsCString& aResult) { + aResult.Truncate(); + + if (aSearchString && aAttributeName) { + // search the string for attributeName + uint32_t attributeNameSize = strlen(aAttributeName); + const char* startOfAttribute = PL_strcasestr(aSearchString, aAttributeName); + if (startOfAttribute && + (*(startOfAttribute - 1) == '?' || *(startOfAttribute - 1) == '&')) { + startOfAttribute += attributeNameSize; // skip over the attributeName + // is there something after the attribute name + if (*startOfAttribute) { + const char* endofAttribute = strchr(startOfAttribute, '&'); + if (endofAttribute) { + aResult.Assign(Substring(startOfAttribute, endofAttribute)); + } else { + aResult.Assign(startOfAttribute); + } + } // if we have a attribute value + } // if we have a attribute name + } // if we got non-null search string and attribute name values +} + +nsresult nsMozIconURI::SetSpecInternal(const nsACString& aSpec) { + // Reset everything to default values. + mIconURL = nullptr; + mSize = DEFAULT_IMAGE_SIZE; + mContentType.Truncate(); + mFileName.Truncate(); + mStockIcon.Truncate(); + mIconSize = -1; + mIconState = -1; + + nsAutoCString iconSpec(aSpec); + if (!Substring(iconSpec, 0, MOZICON_SCHEME_LEN) + .EqualsLiteral(MOZICON_SCHEME) || + (!Substring(iconSpec, MOZICON_SCHEME_LEN, 7).EqualsLiteral("file://") && + // Checking for the leading '//' will match both the '//stock/' and + // '//.foo' cases: + !Substring(iconSpec, MOZICON_SCHEME_LEN, 2).EqualsLiteral("//"))) { + return NS_ERROR_MALFORMED_URI; + } + + int32_t questionMarkPos = iconSpec.Find("?"); + if (questionMarkPos != -1 && + static_cast(iconSpec.Length()) > (questionMarkPos + 1)) { + extractAttributeValue(iconSpec.get(), "contentType=", mContentType); + + nsAutoCString sizeString; + extractAttributeValue(iconSpec.get(), "size=", sizeString); + if (!sizeString.IsEmpty()) { + const char* sizeStr = sizeString.get(); + for (uint32_t i = 0; i < ArrayLength(kSizeStrings); i++) { + if (nsCRT::strcasecmp(sizeStr, kSizeStrings[i]) == 0) { + mIconSize = i; + break; + } + } + + int32_t sizeValue = atoi(sizeString.get()); + if (sizeValue > 0) { + mSize = sizeValue; + } + } + + nsAutoCString stateString; + extractAttributeValue(iconSpec.get(), "state=", stateString); + if (!stateString.IsEmpty()) { + const char* stateStr = stateString.get(); + for (uint32_t i = 0; i < ArrayLength(kStateStrings); i++) { + if (nsCRT::strcasecmp(stateStr, kStateStrings[i]) == 0) { + mIconState = i; + break; + } + } + } + } + + int32_t pathLength = iconSpec.Length() - MOZICON_SCHEME_LEN; + if (questionMarkPos != -1) { + pathLength = questionMarkPos - MOZICON_SCHEME_LEN; + } + if (pathLength < 3) { + return NS_ERROR_MALFORMED_URI; + } + + nsAutoCString iconPath(Substring(iconSpec, MOZICON_SCHEME_LEN, pathLength)); + + // Icon URI path can have three forms: + // (1) //stock/ + // (2) // + // (3) a valid URL + + if (!strncmp("//stock/", iconPath.get(), 8)) { + mStockIcon.Assign(Substring(iconPath, 8)); + // An icon identifier must always be specified. + if (mStockIcon.IsEmpty()) { + return NS_ERROR_MALFORMED_URI; + } + return NS_OK; + } + + if (StringBeginsWith(iconPath, "//"_ns)) { + // Sanity check this supposed dummy file name. + if (iconPath.Length() > SANE_FILE_NAME_LEN) { + return NS_ERROR_MALFORMED_URI; + } + iconPath.Cut(0, 2); + mFileName.Assign(iconPath); + } + + nsresult rv; + nsCOMPtr ioService(do_GetService(NS_IOSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uri; + ioService->NewURI(iconPath, nullptr, nullptr, getter_AddRefs(uri)); + mIconURL = do_QueryInterface(uri); + if (mIconURL) { + // The inner URI should be a 'file:' one. If not, bail. + if (!mIconURL->SchemeIs("file")) { + return NS_ERROR_MALFORMED_URI; + } + mFileName.Truncate(); + } else if (mFileName.IsEmpty()) { + return NS_ERROR_MALFORMED_URI; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetPrePath(nsACString& prePath) { + prePath = MOZICON_SCHEME; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetScheme(nsACString& aScheme) { + aScheme = "moz-icon"; + return NS_OK; +} + +nsresult nsMozIconURI::SetScheme(const nsACString& aScheme) { + // doesn't make sense to set the scheme of a moz-icon URL + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetUsername(nsACString& aUsername) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetUsername(const nsACString& aUsername) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetPassword(nsACString& aPassword) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetPassword(const nsACString& aPassword) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetUserPass(nsACString& aUserPass) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetUserPass(const nsACString& aUserPass) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetHostPort(nsACString& aHostPort) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetHostPort(const nsACString& aHostPort) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetHost(nsACString& aHost) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetHost(const nsACString& aHost) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetPort(int32_t* aPort) { return NS_ERROR_FAILURE; } + +nsresult nsMozIconURI::SetPort(int32_t aPort) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +nsMozIconURI::GetPathQueryRef(nsACString& aPath) { + aPath.Truncate(); + return NS_OK; +} + +nsresult nsMozIconURI::SetPathQueryRef(const nsACString& aPath) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetFilePath(nsACString& aFilePath) { + aFilePath.Truncate(); + return NS_OK; +} + +nsresult nsMozIconURI::SetFilePath(const nsACString& aFilePath) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetQuery(nsACString& aQuery) { + aQuery.Truncate(); + return NS_OK; +} + +nsresult nsMozIconURI::SetQuery(const nsACString& aQuery) { + return NS_ERROR_FAILURE; +} + +nsresult nsMozIconURI::SetQueryWithEncoding(const nsACString& aQuery, + const Encoding* aEncoding) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetRef(nsACString& aRef) { + aRef.Truncate(); + return NS_OK; +} + +nsresult nsMozIconURI::SetRef(const nsACString& aRef) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::Equals(nsIURI* other, bool* result) { + *result = false; + NS_ENSURE_ARG_POINTER(other); + MOZ_ASSERT(result, "null pointer"); + + nsAutoCString spec1; + nsAutoCString spec2; + + nsresult rv = GetSpec(spec1); + NS_ENSURE_SUCCESS(rv, rv); + rv = other->GetSpec(spec2); + NS_ENSURE_SUCCESS(rv, rv); + + if (!nsCRT::strcasecmp(spec1.get(), spec2.get())) { + *result = true; + } else { + *result = false; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::EqualsExceptRef(nsIURI* other, bool* result) { + // GetRef/SetRef not supported by nsMozIconURI, so + // EqualsExceptRef() is the same as Equals(). + return Equals(other, result); +} + +NS_IMETHODIMP +nsMozIconURI::SchemeIs(const char* aScheme, bool* aEquals) { + MOZ_ASSERT(aEquals, "null pointer"); + if (!aScheme) { + *aEquals = false; + return NS_OK; + } + + *aEquals = nsCRT::strcasecmp("moz-icon", aScheme) == 0; + return NS_OK; +} + +nsresult nsMozIconURI::Clone(nsIURI** result) { + nsCOMPtr newIconURL; + if (mIconURL) { + newIconURL = mIconURL; + } + + RefPtr uri = new nsMozIconURI(); + newIconURL.swap(uri->mIconURL); + uri->mSize = mSize; + uri->mContentType = mContentType; + uri->mFileName = mFileName; + uri->mStockIcon = mStockIcon; + uri->mIconSize = mIconSize; + uri->mIconState = mIconState; + uri.forget(result); + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::Resolve(const nsACString& relativePath, nsACString& result) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMozIconURI::GetAsciiSpec(nsACString& aSpecA) { return GetSpec(aSpecA); } + +NS_IMETHODIMP +nsMozIconURI::GetAsciiHostPort(nsACString& aHostPortA) { + return GetHostPort(aHostPortA); +} + +NS_IMETHODIMP +nsMozIconURI::GetAsciiHost(nsACString& aHostA) { return GetHost(aHostA); } + +//////////////////////////////////////////////////////////////////////////////// +// nsIIconUri methods: + +NS_IMETHODIMP +nsMozIconURI::GetIconURL(nsIURL** aFileUrl) { + *aFileUrl = mIconURL; + NS_IF_ADDREF(*aFileUrl); + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetImageSize(uint32_t* aImageSize) +// measured by # of pixels in a row. defaults to 16. +{ + *aImageSize = mSize; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetContentType(nsACString& aContentType) { + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetFileExtension(nsACString& aFileExtension) { + // First, try to get the extension from mIconURL if we have one + if (mIconURL) { + nsAutoCString fileExt; + if (NS_SUCCEEDED(mIconURL->GetFileExtension(fileExt))) { + if (!fileExt.IsEmpty()) { + // unfortunately, this code doesn't give us the required '.' in + // front of the extension so we have to do it ourselves. + aFileExtension.Assign('.'); + aFileExtension.Append(fileExt); + } + } + return NS_OK; + } + + if (!mFileName.IsEmpty()) { + // truncate the extension out of the file path... + const char* chFileName = mFileName.get(); // get the underlying buffer + const char* fileExt = strrchr(chFileName, '.'); + if (!fileExt) { + return NS_OK; + } + aFileExtension = fileExt; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetStockIcon(nsACString& aStockIcon) { + aStockIcon = mStockIcon; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetIconSize(nsACString& aSize) { + if (mIconSize >= 0) { + aSize = kSizeStrings[mIconSize]; + } else { + aSize.Truncate(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetIconState(nsACString& aState) { + if (mIconState >= 0) { + aState = kStateStrings[mIconState]; + } else { + aState.Truncate(); + } + return NS_OK; +} + +void nsMozIconURI::Serialize(URIParams& aParams) { + IconURIParams params; + + if (mIconURL) { + URIParams iconURLParams; + SerializeURI(mIconURL, iconURLParams); + if (iconURLParams.type() == URIParams::T__None) { + // Serialization failed, bail. + return; + } + + params.uri() = Some(std::move(iconURLParams)); + } else { + params.uri() = Nothing(); + } + + params.size() = mSize; + params.fileName() = mFileName; + params.stockIcon() = mStockIcon; + params.iconSize() = mIconSize; + params.iconState() = mIconState; + + aParams = params; +} + +bool nsMozIconURI::Deserialize(const URIParams& aParams) { + if (aParams.type() != URIParams::TIconURIParams) { + MOZ_ASSERT_UNREACHABLE("Received unknown URI from other process!"); + return false; + } + + const IconURIParams& params = aParams.get_IconURIParams(); + if (params.uri().isSome()) { + nsCOMPtr uri = DeserializeURI(params.uri().ref()); + mIconURL = do_QueryInterface(uri); + if (!mIconURL) { + MOZ_ASSERT_UNREACHABLE("bad nsIURI passed"); + return false; + } + } + + mSize = params.size(); + mContentType = params.contentType(); + mFileName = params.fileName(); + mStockIcon = params.stockIcon(); + + if (params.iconSize() < -1 || + params.iconSize() >= (int32_t)ArrayLength(kSizeStrings)) { + return false; + } + mIconSize = params.iconSize(); + + if (params.iconState() < -1 || + params.iconState() >= (int32_t)ArrayLength(kStateStrings)) { + return false; + } + mIconState = params.iconState(); + + return true; +} + +NS_IMETHODIMP +nsMozIconURI::GetInnerURI(nsIURI** aURI) { + nsCOMPtr iconURL = mIconURL; + if (!iconURL) { + *aURI = nullptr; + return NS_ERROR_FAILURE; + } + + iconURL.forget(aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetInnermostURI(nsIURI** aURI) { + return NS_ImplGetInnermostURI(this, aURI); +} + +NS_IMETHODIMP +nsMozIconURI::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT_UNREACHABLE("Use nsIURIMutator.read() instead"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsMozIconURI::ReadPrivate(nsIObjectInputStream* aStream) { + nsAutoCString spec; + nsresult rv = aStream->ReadCString(spec); + NS_ENSURE_SUCCESS(rv, rv); + return SetSpecInternal(spec); +} + +NS_IMETHODIMP +nsMozIconURI::Write(nsIObjectOutputStream* aStream) { + nsAutoCString spec; + nsresult rv = GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + return aStream->WriteStringZ(spec.get()); +} diff --git a/image/decoders/icon/nsIconURI.h b/image/decoders/icon/nsIconURI.h new file mode 100644 index 0000000000..1f55bc686c --- /dev/null +++ b/image/decoders/icon/nsIconURI.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_icon_nsIconURI_h +#define mozilla_image_decoders_icon_nsIconURI_h + +#include "nsIIconURI.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsINestedURI.h" +#include "nsIURIMutator.h" +#include "nsISerializable.h" + +#define NS_THIS_ICONURI_IMPLEMENTATION_CID \ + { /* 0b9bb0c2-fee6-470b-b9b9-9fd9462b5e19 */ \ + 0x5c3e417f, 0xb686, 0x4105, { \ + 0x86, 0xe7, 0xf9, 0x1b, 0xac, 0x97, 0x4d, 0x5c \ + } \ + } + +namespace mozilla { +class Encoding; +} + +class nsMozIconURI final : public nsIMozIconURI, + public nsINestedURI, + public nsISerializable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURI + NS_DECL_NSIMOZICONURI + NS_DECL_NSINESTEDURI + NS_DECL_NSISERIALIZABLE + + protected: + nsMozIconURI(); + virtual ~nsMozIconURI(); + nsCOMPtr mIconURL; // a URL that we want the icon for + uint32_t mSize; // the # of pixels in a row that we want for this image. + // Typically 16, 32, 128, etc. + nsCString mContentType; // optional field explicitly specifying the content + // type + nsCString mFileName; // for if we don't have an actual file path, we're just + // given a filename with an extension + nsCString mStockIcon; + int32_t mIconSize; // -1 if not specified, otherwise index into + // kSizeStrings + int32_t mIconState; // -1 if not specified, otherwise index into + // kStateStrings + + private: + nsresult Clone(nsIURI** aURI); + nsresult SetSpecInternal(const nsACString& input); + nsresult SetScheme(const nsACString& input); + nsresult SetUserPass(const nsACString& input); + nsresult SetUsername(const nsACString& input); + nsresult SetPassword(const nsACString& input); + nsresult SetHostPort(const nsACString& aValue); + nsresult SetHost(const nsACString& input); + nsresult SetPort(int32_t port); + nsresult SetPathQueryRef(const nsACString& input); + nsresult SetRef(const nsACString& input); + nsresult SetFilePath(const nsACString& input); + nsresult SetQuery(const nsACString& input); + nsresult SetQueryWithEncoding(const nsACString& input, + const mozilla::Encoding* encoding); + nsresult ReadPrivate(nsIObjectInputStream* stream); + bool Deserialize(const mozilla::ipc::URIParams&); + + public: + class Mutator final : public nsIURIMutator, + public BaseURIMutator, + public nsISerializable { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + + NS_IMETHOD + Write(nsIObjectOutputStream* aOutputStream) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + [[nodiscard]] NS_IMETHOD Read(nsIObjectInputStream* aStream) override { + return InitFromInputStream(aStream); + } + + NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override { + return InitFromIPCParams(aParams); + } + + NS_IMETHOD Finalize(nsIURI** aURI) override { + mURI.forget(aURI); + return NS_OK; + } + + NS_IMETHOD SetSpec(const nsACString& aSpec, + nsIURIMutator** aMutator) override { + if (aMutator) { + nsCOMPtr mutator = this; + mutator.forget(aMutator); + } + return InitFromSpec(aSpec); + } + + explicit Mutator() {} + + private: + virtual ~Mutator() {} + + friend class nsMozIconURI; + }; + + friend BaseURIMutator; +}; + +#endif // mozilla_image_decoders_icon_nsIconURI_h diff --git a/image/decoders/icon/win/moz.build b/image/decoders/icon/win/moz.build new file mode 100644 index 0000000000..c4472f89ac --- /dev/null +++ b/image/decoders/icon/win/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsIconChannel.cpp", +] + +EXPORTS += [ + "nsIconChannel.h", +] + +LOCAL_INCLUDES += [ + "/image", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/image/decoders/icon/win/nsIconChannel.cpp b/image/decoders/icon/win/nsIconChannel.cpp new file mode 100644 index 0000000000..fe76afe9b1 --- /dev/null +++ b/image/decoders/icon/win/nsIconChannel.cpp @@ -0,0 +1,1006 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Monitor.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/ByteBuf.h" + +#include "nsComponentManagerUtils.h" +#include "nsIconChannel.h" +#include "nsIIconURI.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsMimeTypes.h" +#include "nsIURL.h" +#include "nsIPipe.h" +#include "nsNetCID.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIIconURI.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIMIMEService.h" +#include "nsCExternalHandlerService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsProxyRelease.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" + +#include "Decoder.h" +#include "DecodePool.h" + +// we need windows.h to read out registry information... +#include +#include +#include +#include +#include + +using namespace mozilla; +using namespace mozilla::image; + +using mozilla::ipc::ByteBuf; + +struct ICONFILEHEADER { + uint16_t ifhReserved; + uint16_t ifhType; + uint16_t ifhCount; +}; + +struct ICONENTRY { + int8_t ieWidth; + int8_t ieHeight; + uint8_t ieColors; + uint8_t ieReserved; + uint16_t iePlanes; + uint16_t ieBitCount; + uint32_t ieSizeImage; + uint32_t ieFileOffset; +}; + +struct IconPathInfo { + nsCOMPtr localFile; + nsAutoString filePath; + UINT infoFlags = 0; +}; + +using HIconPromise = MozPromise; + +static UINT GetSizeInfoFlag(uint32_t aDesiredImageSize) { + return aDesiredImageSize > 16 ? SHGFI_SHELLICONSIZE : SHGFI_SMALLICON; +} + +static nsresult ExtractIconInfoFromUrl(nsIURI* aUrl, nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, + nsCString& aContentType, + nsCString& aFileExtension) { + nsresult rv = NS_OK; + nsCOMPtr iconURI(do_QueryInterface(aUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + iconURI->GetImageSize(aDesiredImageSize); + iconURI->GetContentType(aContentType); + iconURI->GetFileExtension(aFileExtension); + + nsCOMPtr url; + rv = iconURI->GetIconURL(getter_AddRefs(url)); + if (NS_FAILED(rv) || !url) return NS_OK; + + nsCOMPtr fileURL = do_QueryInterface(url, &rv); + if (NS_FAILED(rv) || !fileURL) return NS_OK; + + nsCOMPtr file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) return NS_OK; + + return file->Clone(aLocalFile); +} + +static nsresult ExtractIconPathInfoFromUrl(nsIURI* aUrl, + IconPathInfo* aIconPathInfo) { + nsCString contentType; + nsCString fileExt; + nsCOMPtr localFile; // file we want an icon for + uint32_t desiredImageSize; + nsresult rv = ExtractIconInfoFromUrl(aUrl, getter_AddRefs(localFile), + &desiredImageSize, contentType, fileExt); + NS_ENSURE_SUCCESS(rv, rv); + + // if the file exists, we are going to use it's real attributes... + // otherwise we only want to use it for it's extension... + UINT infoFlags = SHGFI_ICON; + + bool fileExists = false; + + nsAutoString filePath; + CopyASCIItoUTF16(fileExt, filePath); + if (localFile) { + rv = localFile->Normalize(); + NS_ENSURE_SUCCESS(rv, rv); + + localFile->GetPath(filePath); + if (filePath.Length() < 2 || filePath[1] != ':') { + return NS_ERROR_MALFORMED_URI; // UNC + } + + if (filePath.Last() == ':') { + filePath.Append('\\'); + } else { + localFile->Exists(&fileExists); + if (!fileExists) { + localFile->GetLeafName(filePath); + } + } + } + + if (!fileExists) { + infoFlags |= SHGFI_USEFILEATTRIBUTES; + } + + infoFlags |= GetSizeInfoFlag(desiredImageSize); + + // if we have a content type... then use it! but for existing files, + // we want to show their real icon. + if (!fileExists && !contentType.IsEmpty()) { + nsCOMPtr mimeService( + do_GetService(NS_MIMESERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString defFileExt; + mimeService->GetPrimaryExtension(contentType, fileExt, defFileExt); + // If the mime service does not know about this mime type, we show + // the generic icon. + // In any case, we need to insert a '.' before the extension. + filePath = u"."_ns + NS_ConvertUTF8toUTF16(defFileExt); + } + + if (!localFile && !fileExists && + ((filePath.Length() == 1 && filePath.Last() == '.') || + filePath.Length() == 0)) { + filePath = u".MozBogusExtensionMoz"_ns; + } + + aIconPathInfo->localFile = std::move(localFile); + aIconPathInfo->filePath = std::move(filePath); + aIconPathInfo->infoFlags = infoFlags; + + return NS_OK; +} + +static bool GetSpecialFolderIcon(nsIFile* aFile, int aFolder, UINT aInfoFlags, + HICON* aIcon) { + if (!aFile) { + return false; + } + + wchar_t fileNativePath[MAX_PATH]; + nsAutoString fileNativePathStr; + aFile->GetPath(fileNativePathStr); + ::GetShortPathNameW(fileNativePathStr.get(), fileNativePath, + ArrayLength(fileNativePath)); + + struct IdListDeleter { + void operator()(ITEMIDLIST* ptr) { ::CoTaskMemFree(ptr); } + }; + + UniquePtr idList; + HRESULT hr = + ::SHGetSpecialFolderLocation(nullptr, aFolder, getter_Transfers(idList)); + if (FAILED(hr)) { + return false; + } + + wchar_t specialNativePath[MAX_PATH]; + ::SHGetPathFromIDListW(idList.get(), specialNativePath); + ::GetShortPathNameW(specialNativePath, specialNativePath, + ArrayLength(specialNativePath)); + + if (wcsicmp(fileNativePath, specialNativePath) != 0) { + return false; + } + + SHFILEINFOW sfi = {}; + aInfoFlags |= (SHGFI_PIDL | SHGFI_SYSICONINDEX); + if (::SHGetFileInfoW((LPCWSTR)(LPCITEMIDLIST)idList.get(), 0, &sfi, + sizeof(sfi), aInfoFlags) == 0) { + return false; + } + + *aIcon = sfi.hIcon; + return true; +} + +static nsresult GetIconHandleFromPathInfo(const IconPathInfo& aPathInfo, + HICON* aIcon) { + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown()); + + // Is this the "Desktop" folder? + if (GetSpecialFolderIcon(aPathInfo.localFile, CSIDL_DESKTOP, + aPathInfo.infoFlags, aIcon)) { + return NS_OK; + } + + // Is this the "My Documents" folder? + if (GetSpecialFolderIcon(aPathInfo.localFile, CSIDL_PERSONAL, + aPathInfo.infoFlags, aIcon)) { + return NS_OK; + } + + // There are other "Special Folders" and Namespace entities that we + // are not fetching icons for, see: + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ + // shellcc/platform/shell/reference/enums/csidl.asp + // If we ever need to get them, code to do so would be inserted here. + + // Not a special folder, or something else failed above. + SHFILEINFOW sfi = {}; + if (::SHGetFileInfoW(aPathInfo.filePath.get(), FILE_ATTRIBUTE_ARCHIVE, &sfi, + sizeof(sfi), aPathInfo.infoFlags) != 0) { + *aIcon = sfi.hIcon; + return NS_OK; + } + + return NS_ERROR_NOT_AVAILABLE; +} + +// Match stock icons with names +static mozilla::Maybe GetStockIconIDForName( + const nsACString& aStockName) { + return aStockName.EqualsLiteral("uac-shield") ? Some(SIID_SHIELD) : Nothing(); +} + +// Specific to Vista and above +static nsresult GetStockHIcon(nsIMozIconURI* aIconURI, HICON* aIcon) { + uint32_t desiredImageSize; + aIconURI->GetImageSize(&desiredImageSize); + nsAutoCString stockIcon; + aIconURI->GetStockIcon(stockIcon); + + Maybe stockIconID = GetStockIconIDForName(stockIcon); + if (stockIconID.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + UINT infoFlags = SHGSI_ICON; + infoFlags |= GetSizeInfoFlag(desiredImageSize); + + SHSTOCKICONINFO sii = {0}; + sii.cbSize = sizeof(sii); + HRESULT hr = SHGetStockIconInfo(*stockIconID, infoFlags, &sii); + if (FAILED(hr)) { + return NS_ERROR_FAILURE; + } + + *aIcon = sii.hIcon; + + return NS_OK; +} + +// Given a BITMAPINFOHEADER, returns the size of the color table. +static int GetColorTableSize(BITMAPINFOHEADER* aHeader) { + int colorTableSize = -1; + + // http://msdn.microsoft.com/en-us/library/dd183376%28v=VS.85%29.aspx + switch (aHeader->biBitCount) { + case 0: + colorTableSize = 0; + break; + case 1: + colorTableSize = 2 * sizeof(RGBQUAD); + break; + case 4: + case 8: { + // The maximum possible size for the color table is 2**bpp, so check for + // that and fail if we're not in those bounds + unsigned int maxEntries = 1 << (aHeader->biBitCount); + if (aHeader->biClrUsed > 0 && aHeader->biClrUsed <= maxEntries) { + colorTableSize = aHeader->biClrUsed * sizeof(RGBQUAD); + } else if (aHeader->biClrUsed == 0) { + colorTableSize = maxEntries * sizeof(RGBQUAD); + } + break; + } + case 16: + case 32: + // If we have BI_BITFIELDS compression, we would normally need 3 DWORDS + // for the bitfields mask which would be stored in the color table; + // However, we instead force the bitmap to request data of type BI_RGB so + // the color table should be of size 0. Setting aHeader->biCompression = + // BI_RGB forces the later call to GetDIBits to return to us BI_RGB data. + if (aHeader->biCompression == BI_BITFIELDS) { + aHeader->biCompression = BI_RGB; + } + colorTableSize = 0; + break; + case 24: + colorTableSize = 0; + break; + } + + if (colorTableSize < 0) { + NS_WARNING("Unable to figure out the color table size for this bitmap"); + } + + return colorTableSize; +} + +// Given a header and a size, creates a freshly allocated BITMAPINFO structure. +// It is the caller's responsibility to null-check and delete the structure. +static BITMAPINFO* CreateBitmapInfo(BITMAPINFOHEADER* aHeader, + size_t aColorTableSize) { + BITMAPINFO* bmi = (BITMAPINFO*)::operator new( + sizeof(BITMAPINFOHEADER) + aColorTableSize, mozilla::fallible); + if (bmi) { + memcpy(bmi, aHeader, sizeof(BITMAPINFOHEADER)); + memset(bmi->bmiColors, 0, aColorTableSize); + } + return bmi; +} + +static nsresult MakeIconBuffer(HICON aIcon, ByteBuf* aOutBuffer) { + nsresult rv = NS_ERROR_FAILURE; + + if (aIcon) { + // we got a handle to an icon. Now we want to get a bitmap for the icon + // using GetIconInfo.... + ICONINFO iconInfo; + if (GetIconInfo(aIcon, &iconInfo)) { + // we got the bitmaps, first find out their size + HDC hDC = CreateCompatibleDC(nullptr); // get a device context for + // the screen. + BITMAPINFOHEADER maskHeader = {sizeof(BITMAPINFOHEADER)}; + BITMAPINFOHEADER colorHeader = {sizeof(BITMAPINFOHEADER)}; + int colorTableSize, maskTableSize; + if (GetDIBits(hDC, iconInfo.hbmMask, 0, 0, nullptr, + (BITMAPINFO*)&maskHeader, DIB_RGB_COLORS) && + GetDIBits(hDC, iconInfo.hbmColor, 0, 0, nullptr, + (BITMAPINFO*)&colorHeader, DIB_RGB_COLORS) && + maskHeader.biHeight == colorHeader.biHeight && + maskHeader.biWidth == colorHeader.biWidth && + colorHeader.biBitCount > 8 && colorHeader.biSizeImage > 0 && + colorHeader.biWidth >= 0 && colorHeader.biWidth <= 255 && + colorHeader.biHeight >= 0 && colorHeader.biHeight <= 255 && + maskHeader.biSizeImage > 0 && + (colorTableSize = GetColorTableSize(&colorHeader)) >= 0 && + (maskTableSize = GetColorTableSize(&maskHeader)) >= 0) { + uint32_t iconSize = sizeof(ICONFILEHEADER) + sizeof(ICONENTRY) + + sizeof(BITMAPINFOHEADER) + colorHeader.biSizeImage + + maskHeader.biSizeImage; + + if (!aOutBuffer->Allocate(iconSize)) { + rv = NS_ERROR_OUT_OF_MEMORY; + } else { + uint8_t* whereTo = aOutBuffer->mData; + int howMuch; + + // the data starts with an icon file header + ICONFILEHEADER iconHeader; + iconHeader.ifhReserved = 0; + iconHeader.ifhType = 1; + iconHeader.ifhCount = 1; + howMuch = sizeof(ICONFILEHEADER); + memcpy(whereTo, &iconHeader, howMuch); + whereTo += howMuch; + + // followed by the single icon entry + ICONENTRY iconEntry; + iconEntry.ieWidth = static_cast(colorHeader.biWidth); + iconEntry.ieHeight = static_cast(colorHeader.biHeight); + iconEntry.ieColors = 0; + iconEntry.ieReserved = 0; + iconEntry.iePlanes = 1; + iconEntry.ieBitCount = colorHeader.biBitCount; + iconEntry.ieSizeImage = sizeof(BITMAPINFOHEADER) + + colorHeader.biSizeImage + + maskHeader.biSizeImage; + iconEntry.ieFileOffset = sizeof(ICONFILEHEADER) + sizeof(ICONENTRY); + howMuch = sizeof(ICONENTRY); + memcpy(whereTo, &iconEntry, howMuch); + whereTo += howMuch; + + // followed by the bitmap info header + // (doubling the height because icons have two bitmaps) + colorHeader.biHeight *= 2; + colorHeader.biSizeImage += maskHeader.biSizeImage; + howMuch = sizeof(BITMAPINFOHEADER); + memcpy(whereTo, &colorHeader, howMuch); + whereTo += howMuch; + colorHeader.biHeight /= 2; + colorHeader.biSizeImage -= maskHeader.biSizeImage; + + // followed by the XOR bitmap data (colorHeader) + // (you'd expect the color table to come here, but it apparently + // doesn't) + BITMAPINFO* colorInfo = + CreateBitmapInfo(&colorHeader, colorTableSize); + if (colorInfo && + GetDIBits(hDC, iconInfo.hbmColor, 0, colorHeader.biHeight, + whereTo, colorInfo, DIB_RGB_COLORS)) { + whereTo += colorHeader.biSizeImage; + + // and finally the AND bitmap data (maskHeader) + BITMAPINFO* maskInfo = CreateBitmapInfo(&maskHeader, maskTableSize); + if (maskInfo && + GetDIBits(hDC, iconInfo.hbmMask, 0, maskHeader.biHeight, + whereTo, maskInfo, DIB_RGB_COLORS)) { + rv = NS_OK; + } // if we got bitmap bits + delete maskInfo; + } // if we got mask bits + delete colorInfo; + } // if we allocated the buffer + } // if we got mask size + + DeleteDC(hDC); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + } // if we got icon info + DestroyIcon(aIcon); + } // if we got an hIcon + + return rv; +} + +static nsresult GetIconHandleFromURLBlocking(nsIMozIconURI* aUrl, + HICON* aIcon) { + nsAutoCString stockIcon; + aUrl->GetStockIcon(stockIcon); + if (!stockIcon.IsEmpty()) { + return GetStockHIcon(aUrl, aIcon); + } + + IconPathInfo iconPathInfo; + nsresult rv = ExtractIconPathInfoFromUrl(aUrl, &iconPathInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr task = NS_NewRunnableFunction( + "GetIconHandleFromURLBlocking", + [&] { rv = GetIconHandleFromPathInfo(iconPathInfo, aIcon); }); + + RefPtr target = DecodePool::Singleton()->GetIOEventTarget(); + + nsresult dispatchResult = SyncRunnable::DispatchToThread(target, task); + NS_ENSURE_SUCCESS(dispatchResult, dispatchResult); + + return rv; +} + +static RefPtr GetIconHandleFromURLAsync(nsIMozIconURI* aUrl) { + RefPtr promise = new HIconPromise::Private(__func__); + + nsAutoCString stockIcon; + aUrl->GetStockIcon(stockIcon); + if (!stockIcon.IsEmpty()) { + HICON hIcon = nullptr; + nsresult rv = GetStockHIcon(aUrl, &hIcon); + if (NS_SUCCEEDED(rv)) { + promise->Resolve(hIcon, __func__); + } else { + promise->Reject(rv, __func__); + } + return promise; + } + + IconPathInfo iconPathInfo; + nsresult rv = ExtractIconPathInfoFromUrl(aUrl, &iconPathInfo); + if (NS_FAILED(rv)) { + promise->Reject(rv, __func__); + return promise; + } + + nsCOMPtr task = NS_NewRunnableFunction( + "GetIconHandleFromURLAsync", [iconPathInfo, promise] { + HICON hIcon = nullptr; + nsresult rv = GetIconHandleFromPathInfo(iconPathInfo, &hIcon); + if (NS_SUCCEEDED(rv)) { + promise->Resolve(hIcon, __func__); + } else { + promise->Reject(rv, __func__); + } + }); + + RefPtr target = DecodePool::Singleton()->GetIOEventTarget(); + + rv = target->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + promise->Reject(rv, __func__); + } + + return promise; +} + +static RefPtr GetIconBufferFromURLAsync( + nsIMozIconURI* aUrl) { + RefPtr promise = + new nsIconChannel::ByteBufPromise::Private(__func__); + + GetIconHandleFromURLAsync(aUrl)->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](HICON aIcon) { + ByteBuf iconBuffer; + nsresult rv = MakeIconBuffer(aIcon, &iconBuffer); + if (NS_SUCCEEDED(rv)) { + promise->Resolve(std::move(iconBuffer), __func__); + } else { + promise->Reject(rv, __func__); + } + }, + [promise](nsresult rv) { promise->Reject(rv, __func__); }); + + return promise; +} + +static nsresult WriteByteBufToOutputStream(const ByteBuf& aBuffer, + nsIAsyncOutputStream* aStream) { + uint32_t written = 0; + nsresult rv = aStream->Write(reinterpret_cast(aBuffer.mData), + aBuffer.mLen, &written); + NS_ENSURE_SUCCESS(rv, rv); + + return (written == aBuffer.mLen) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +NS_IMPL_ISUPPORTS(nsIconChannel, nsIChannel, nsIRequest, nsIRequestObserver, + nsIStreamListener) + +// nsIconChannel methods +nsIconChannel::nsIconChannel() {} + +nsIconChannel::~nsIconChannel() { + if (mLoadInfo) { + NS_ReleaseOnMainThread("nsIconChannel::mLoadInfo", mLoadInfo.forget()); + } + if (mLoadGroup) { + NS_ReleaseOnMainThread("nsIconChannel::mLoadGroup", mLoadGroup.forget()); + } +} + +nsresult nsIconChannel::Init(nsIURI* uri) { + NS_ASSERTION(uri, "no uri"); + mUrl = uri; + mOriginalURI = uri; + nsresult rv; + mPump = do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +NS_IMETHODIMP +nsIconChannel::GetName(nsACString& result) { return mUrl->GetSpec(result); } + +NS_IMETHODIMP +nsIconChannel::IsPending(bool* result) { return mPump->IsPending(result); } + +NS_IMETHODIMP +nsIconChannel::GetStatus(nsresult* status) { return mPump->GetStatus(status); } + +NS_IMETHODIMP nsIconChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsIconChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsIconChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +nsIconChannel::Cancel(nsresult status) { + mCanceled = true; + return mPump->Cancel(status); +} + +NS_IMETHODIMP +nsIconChannel::GetCanceled(bool* result) { + *result = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::Suspend(void) { return mPump->Suspend(); } + +NS_IMETHODIMP +nsIconChannel::Resume(void) { return mPump->Resume(); } +NS_IMETHODIMP +nsIconChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = mLoadGroup; + NS_IF_ADDREF(*aLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadFlags(uint32_t* aLoadAttributes) { + return mPump->GetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::SetLoadFlags(uint32_t aLoadAttributes) { + return mPump->SetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsIconChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsIconChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIChannel methods: + +NS_IMETHODIMP +nsIconChannel::GetOriginalURI(nsIURI** aURI) { + *aURI = mOriginalURI; + NS_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetURI(nsIURI** aURI) { + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +// static +RefPtr nsIconChannel::GetIconAsync( + nsIURI* aURI) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsresult rv = NS_OK; + nsCOMPtr iconURI(do_QueryInterface(aURI, &rv)); + if (NS_FAILED(rv)) { + return ByteBufPromise::CreateAndReject(rv, __func__); + } + + return GetIconBufferFromURLAsync(iconURI); +} + +NS_IMETHODIMP +nsIconChannel::Open(nsIInputStream** aStream) { + nsCOMPtr listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + // Double-check that we are actually an icon URL + nsCOMPtr iconURI(do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the handle for the given icon URI. This may involve the decode I/O + // thread, as we can only call SHGetFileInfo() from that thread + // + // Since this API is synchronous, this call will not return until the decode + // I/O thread returns with the icon handle + // + // Once we have the handle, we create a Windows ICO buffer with it and + // dump the buffer into the output end of the pipe. The input end will + // be returned to the caller + HICON hIcon = nullptr; + rv = GetIconHandleFromURLBlocking(iconURI, &hIcon); + NS_ENSURE_SUCCESS(rv, rv); + + ByteBuf iconBuffer; + rv = MakeIconBuffer(hIcon, &iconBuffer); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the asynchronous pipe with a blocking read end + nsCOMPtr inputStream; + nsCOMPtr outputStream; + NS_NewPipe2(getter_AddRefs(inputStream), getter_AddRefs(outputStream), + false /*nonBlockingInput*/, false /*nonBlockingOutput*/, + iconBuffer.mLen /*segmentSize*/, 1 /*segmentCount*/); + + rv = WriteByteBufToOutputStream(iconBuffer, outputStream); + + if (NS_SUCCEEDED(rv)) { + inputStream.forget(aStream); + } + + return rv; +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + mListener = listener; + + rv = StartAsyncOpen(); + if (NS_FAILED(rv)) { + mListener = nullptr; + mCallbacks = nullptr; + return rv; + } + + // Add ourself to the load group, if available + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + + return NS_OK; +} + +nsresult nsIconChannel::StartAsyncOpen() { + // Double-check that we are actually an icon URL + nsresult rv = NS_OK; + nsCOMPtr iconURI(do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the asynchronous pipe with a non-blocking read end + nsCOMPtr inputStream; + nsCOMPtr outputStream; + NS_NewPipe2(getter_AddRefs(inputStream), getter_AddRefs(outputStream), + true /*nonBlockingInput*/, false /*nonBlockingOutput*/, + 0 /*segmentSize*/, UINT32_MAX /*segmentCount*/); + + // If we are in content, we asynchronously request the ICO buffer from + // the parent process because the APIs to load icons don't work with + // Win32k Lockdown + using ContentChild = mozilla::dom::ContentChild; + if (auto* contentChild = ContentChild::GetSingleton()) { + RefPtr iconPromise = + contentChild->SendGetSystemIcon(mUrl); + if (!iconPromise) { + return NS_ERROR_UNEXPECTED; + } + + iconPromise->Then( + mozilla::GetCurrentSerialEventTarget(), __func__, + [outputStream](std::tuple>&& aArg) { + nsresult rv = std::get<0>(aArg); + mozilla::Maybe iconBuffer = std::move(std::get<1>(aArg)); + + if (NS_SUCCEEDED(rv)) { + MOZ_RELEASE_ASSERT(iconBuffer); + rv = WriteByteBufToOutputStream(*iconBuffer, outputStream); + } + + outputStream->CloseWithStatus(rv); + }, + [outputStream](mozilla::ipc::ResponseRejectReason) { + outputStream->CloseWithStatus(NS_ERROR_FAILURE); + }); + } else { + // Get the handle for the given icon URI. This may involve the decode I/O + // thread, as we can only call SHGetFileInfo() from that thread + // + // Once we have the handle, we create a Windows ICO buffer with it and + // dump the buffer into the output end of the pipe. The input end will be + // pumped to our attached nsIStreamListener + GetIconBufferFromURLAsync(iconURI)->Then( + GetCurrentSerialEventTarget(), __func__, + [outputStream](ByteBuf aIconBuffer) { + nsresult rv = + WriteByteBufToOutputStream(std::move(aIconBuffer), outputStream); + outputStream->CloseWithStatus(rv); + }, + [outputStream](nsresult rv) { outputStream->CloseWithStatus(rv); }); + } + + // Use the main thread for the pumped events unless the load info + // specifies otherwise + nsCOMPtr listenerTarget = + nsContentUtils::GetEventTargetByLoadInfo(mLoadInfo, + mozilla::TaskCategory::Other); + if (!listenerTarget) { + listenerTarget = do_GetMainThread(); + } + + rv = mPump->Init(inputStream.get(), 0 /*segmentSize*/, 0 /*segmentCount*/, + false /*closeWhenDone*/, listenerTarget); + NS_ENSURE_SUCCESS(rv, rv); + + return mPump->AsyncRead(this); +} + +NS_IMETHODIMP +nsIconChannel::GetContentType(nsACString& aContentType) { + aContentType.AssignLiteral(IMAGE_ICO); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentType(const nsACString& aContentType) { + // It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsIconChannel::GetContentCharset(nsACString& aContentCharset) { + aContentCharset.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentCharset(const nsACString& aContentCharset) { + // It doesn't make sense to set the content-charset on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = 0; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentLength(int64_t aContentLength) { + MOZ_ASSERT_UNREACHABLE("nsIconChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIconChannel::GetOwner(nsISupports** aOwner) { + *aOwner = mOwner.get(); + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOwner(nsISupports* aOwner) { + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + *aNotificationCallbacks = mCallbacks.get(); + NS_IF_ADDREF(*aNotificationCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} + +// nsIRequestObserver methods +NS_IMETHODIMP nsIconChannel::OnStartRequest(nsIRequest* aRequest) { + if (mListener) { + return mListener->OnStartRequest(this); + } + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + if (mListener) { + mListener->OnStopRequest(this, aStatus); + mListener = nullptr; + } + + // Remove from load group + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatus); + } + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + + return NS_OK; +} + +// nsIStreamListener methods +NS_IMETHODIMP +nsIconChannel::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + if (mListener) { + return mListener->OnDataAvailable(this, aStream, aOffset, aCount); + } + return NS_OK; +} diff --git a/image/decoders/icon/win/nsIconChannel.h b/image/decoders/icon/win/nsIconChannel.h new file mode 100644 index 0000000000..4065be76e3 --- /dev/null +++ b/image/decoders/icon/win/nsIconChannel.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_icon_win_nsIconChannel_h +#define mozilla_image_encoders_icon_win_nsIconChannel_h + +#include "mozilla/Attributes.h" +#include "mozilla/MozPromise.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIChannel.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIURI.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" + +namespace mozilla::ipc { +class ByteBuf; +} + +class nsIconChannel final : public nsIChannel, public nsIStreamListener { + public: + using ByteBufPromise = + mozilla::MozPromise; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsIconChannel(); + + nsresult Init(nsIURI* uri); + + /// Obtains an icon in Windows ICO format as a ByteBuf instead + /// of a channel. For use with IPC. + static RefPtr GetIconAsync(nsIURI* aURI); + + private: + ~nsIconChannel(); + + nsresult StartAsyncOpen(); + + nsCOMPtr mUrl; + nsCOMPtr mOriginalURI; + nsCOMPtr mLoadGroup; + nsCOMPtr mCallbacks; + nsCOMPtr mOwner; + nsCOMPtr mLoadInfo; + + nsCOMPtr mPump; + nsCOMPtr mListener; + + bool mCanceled = false; +}; + +#endif // mozilla_image_encoders_icon_win_nsIconChannel_h diff --git a/image/decoders/moz.build b/image/decoders/moz.build new file mode 100644 index 0000000000..d7e062f843 --- /dev/null +++ b/image/decoders/moz.build @@ -0,0 +1,62 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"] + +# The Icon Channel stuff really shouldn't live in decoders/icon, but we'll +# fix that another time. +if toolkit == "gtk": + DIRS += ["icon/gtk", "icon"] + +if CONFIG["OS_ARCH"] == "WINNT": + DIRS += ["icon/win", "icon"] + +if toolkit == "cocoa": + DIRS += ["icon/mac", "icon"] +elif toolkit == "android": + DIRS += ["icon/android", "icon"] + +UNIFIED_SOURCES += [ + "EXIF.cpp", + "iccjpeg.c", + "nsBMPDecoder.cpp", + "nsGIFDecoder2.cpp", + "nsICODecoder.cpp", + "nsIconDecoder.cpp", + "nsJPEGDecoder.cpp", + "nsPNGDecoder.cpp", + "nsWebPDecoder.cpp", +] + +if CONFIG["MOZ_AV1"]: + UNIFIED_SOURCES += [ + "nsAVIFDecoder.cpp", + ] + +if CONFIG["MOZ_JXL"]: + UNIFIED_SOURCES += [ + "nsJXLDecoder.cpp", + ] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + # Access to Skia headers for Downscaler. + "/gfx/2d", + # Decoders need ImageLib headers. + "/image", + # for libyuv::ARGBAttenuate and ::ARGBUnattenuate + "/media/libyuv/libyuv/include", +] + +LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"] + +FINAL_LIBRARY = "xul" + +CXXFLAGS += ["-Werror=switch"] + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/image/decoders/nsAVIFDecoder.cpp b/image/decoders/nsAVIFDecoder.cpp new file mode 100644 index 0000000000..06b5a60086 --- /dev/null +++ b/image/decoders/nsAVIFDecoder.cpp @@ -0,0 +1,1991 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first + +#include "nsAVIFDecoder.h" + +#include "aom/aomdx.h" + +#include "DAV1DDecoder.h" +#include "gfxPlatform.h" +#include "YCbCrUtils.h" +#include "libyuv.h" + +#include "SurfacePipeFactory.h" + +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryComms.h" +#include "mozilla/UniquePtrExtensions.h" + +using namespace mozilla::gfx; + +namespace mozilla { + +namespace image { + +using Telemetry::LABELS_AVIF_A1LX; +using Telemetry::LABELS_AVIF_A1OP; +using Telemetry::LABELS_AVIF_ALPHA; +using Telemetry::LABELS_AVIF_AOM_DECODE_ERROR; +using Telemetry::LABELS_AVIF_BIT_DEPTH; +using Telemetry::LABELS_AVIF_CICP_CP; +using Telemetry::LABELS_AVIF_CICP_MC; +using Telemetry::LABELS_AVIF_CICP_TC; +using Telemetry::LABELS_AVIF_CLAP; +using Telemetry::LABELS_AVIF_COLR; +using Telemetry::LABELS_AVIF_DECODE_RESULT; +using Telemetry::LABELS_AVIF_DECODER; +using Telemetry::LABELS_AVIF_GRID; +using Telemetry::LABELS_AVIF_IPRO; +using Telemetry::LABELS_AVIF_ISPE; +using Telemetry::LABELS_AVIF_LSEL; +using Telemetry::LABELS_AVIF_MAJOR_BRAND; +using Telemetry::LABELS_AVIF_PASP; +using Telemetry::LABELS_AVIF_PIXI; +using Telemetry::LABELS_AVIF_SEQUENCE; +using Telemetry::LABELS_AVIF_YUV_COLOR_SPACE; + +static LazyLogModule sAVIFLog("AVIFDecoder"); + +static const LABELS_AVIF_BIT_DEPTH gColorDepthLabel[] = { + LABELS_AVIF_BIT_DEPTH::color_8, LABELS_AVIF_BIT_DEPTH::color_10, + LABELS_AVIF_BIT_DEPTH::color_12, LABELS_AVIF_BIT_DEPTH::color_16}; + +static const LABELS_AVIF_YUV_COLOR_SPACE gColorSpaceLabel[] = { + LABELS_AVIF_YUV_COLOR_SPACE::BT601, LABELS_AVIF_YUV_COLOR_SPACE::BT709, + LABELS_AVIF_YUV_COLOR_SPACE::BT2020, LABELS_AVIF_YUV_COLOR_SPACE::identity}; + +static MaybeIntSize GetImageSize(const Mp4parseAvifInfo& aInfo) { + // Note this does not take cropping via CleanAperture (clap) into account + const struct Mp4parseImageSpatialExtents* ispe = aInfo.spatial_extents; + + if (ispe) { + // Decoder::PostSize takes int32_t, but ispe contains uint32_t + CheckedInt width = ispe->image_width; + CheckedInt height = ispe->image_height; + + if (width.isValid() && height.isValid()) { + return Some(IntSize{width.value(), height.value()}); + } + } + + return Nothing(); +} + +// Translate the MIAF/HEIF-based orientation transforms (imir, irot) into +// ImageLib's representation. Note that the interpretation of imir was reversed +// Between HEIF (ISO 23008-12:2017) and ISO/IEC 23008-12:2017/DAmd 2. This is +// handled by mp4parse. See mp4parse::read_imir for details. +Orientation GetImageOrientation(const Mp4parseAvifInfo& aInfo) { + // Per MIAF (ISO/IEC 23000-22:2019) § 7.3.6.7 + // These properties, if used, shall be indicated to be applied in the + // following order: clean aperture first, then rotation, then mirror. + // The Orientation type does the same order, but opposite rotation direction + + const Mp4parseIrot heifRot = aInfo.image_rotation; + const Mp4parseImir* heifMir = aInfo.image_mirror; + Angle mozRot; + Flip mozFlip; + + if (!heifMir) { // No mirroring + mozFlip = Flip::Unflipped; + + switch (heifRot) { + case MP4PARSE_IROT_D0: + // ⥠ UPWARDS HARPOON WITH BARB LEFT FROM BAR + mozRot = Angle::D0; + break; + case MP4PARSE_IROT_D90: + // ⥞ LEFTWARDS HARPOON WITH BARB DOWN FROM BAR + mozRot = Angle::D270; + break; + case MP4PARSE_IROT_D180: + // ⥠DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR + mozRot = Angle::D180; + break; + case MP4PARSE_IROT_D270: + // ⥛ RIGHTWARDS HARPOON WITH BARB UP FROM BAR + mozRot = Angle::D90; + break; + default: + MOZ_ASSERT_UNREACHABLE(); + } + } else { + MOZ_ASSERT(heifMir); + mozFlip = Flip::Horizontal; + + enum class HeifFlippedOrientation : uint8_t { + IROT_D0_IMIR_V = (MP4PARSE_IROT_D0 << 1) | MP4PARSE_IMIR_LEFT_RIGHT, + IROT_D0_IMIR_H = (MP4PARSE_IROT_D0 << 1) | MP4PARSE_IMIR_TOP_BOTTOM, + IROT_D90_IMIR_V = (MP4PARSE_IROT_D90 << 1) | MP4PARSE_IMIR_LEFT_RIGHT, + IROT_D90_IMIR_H = (MP4PARSE_IROT_D90 << 1) | MP4PARSE_IMIR_TOP_BOTTOM, + IROT_D180_IMIR_V = (MP4PARSE_IROT_D180 << 1) | MP4PARSE_IMIR_LEFT_RIGHT, + IROT_D180_IMIR_H = (MP4PARSE_IROT_D180 << 1) | MP4PARSE_IMIR_TOP_BOTTOM, + IROT_D270_IMIR_V = (MP4PARSE_IROT_D270 << 1) | MP4PARSE_IMIR_LEFT_RIGHT, + IROT_D270_IMIR_H = (MP4PARSE_IROT_D270 << 1) | MP4PARSE_IMIR_TOP_BOTTOM, + }; + + HeifFlippedOrientation heifO = + HeifFlippedOrientation((heifRot << 1) | *heifMir); + + switch (heifO) { + case HeifFlippedOrientation::IROT_D0_IMIR_V: + case HeifFlippedOrientation::IROT_D180_IMIR_H: + // ⥜ UPWARDS HARPOON WITH BARB RIGHT FROM BAR + mozRot = Angle::D0; + break; + case HeifFlippedOrientation::IROT_D270_IMIR_V: + case HeifFlippedOrientation::IROT_D90_IMIR_H: + // ⥚ LEFTWARDS HARPOON WITH BARB UP FROM BAR + mozRot = Angle::D90; + break; + case HeifFlippedOrientation::IROT_D180_IMIR_V: + case HeifFlippedOrientation::IROT_D0_IMIR_H: + // ⥡ DOWNWARDS HARPOON WITH BARB LEFT FROM BAR + mozRot = Angle::D180; + break; + case HeifFlippedOrientation::IROT_D90_IMIR_V: + case HeifFlippedOrientation::IROT_D270_IMIR_H: + // ⥟ RIGHTWARDS HARPOON WITH BARB DOWN FROM BAR + mozRot = Angle::D270; + break; + default: + MOZ_ASSERT_UNREACHABLE(); + } + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("GetImageOrientation: (rot%d, imir(%s)) -> (Angle%d, " + "Flip%d)", + static_cast(heifRot), + heifMir ? (*heifMir == MP4PARSE_IMIR_LEFT_RIGHT ? "left-right" + : "top-bottom") + : "none", + static_cast(mozRot), static_cast(mozFlip))); + return Orientation{mozRot, mozFlip}; +} +bool AVIFDecoderStream::ReadAt(int64_t offset, void* data, size_t size, + size_t* bytes_read) { + size = std::min(size, size_t(mBuffer->length() - offset)); + + if (size <= 0) { + return false; + } + + memcpy(data, mBuffer->begin() + offset, size); + *bytes_read = size; + return true; +} + +bool AVIFDecoderStream::Length(int64_t* size) { + *size = + static_cast(std::min(mBuffer->length(), INT64_MAX)); + return true; +} + +const uint8_t* AVIFDecoderStream::GetContiguousAccess(int64_t aOffset, + size_t aSize) { + if (aOffset + aSize >= mBuffer->length()) { + return nullptr; + } + + return mBuffer->begin() + aOffset; +} + +AVIFParser::~AVIFParser() { + MOZ_LOG(sAVIFLog, LogLevel::Debug, ("Destroy AVIFParser=%p", this)); +} + +Mp4parseStatus AVIFParser::Create(const Mp4parseIo* aIo, ByteStream* aBuffer, + UniquePtr& aParserOut, + bool aAllowSequences, + bool aAnimateAVIFMajor) { + MOZ_ASSERT(aIo); + MOZ_ASSERT(!aParserOut); + + UniquePtr p(new AVIFParser(aIo)); + Mp4parseStatus status = p->Init(aBuffer, aAllowSequences, aAnimateAVIFMajor); + + if (status == MP4PARSE_STATUS_OK) { + MOZ_ASSERT(p->mParser); + aParserOut = std::move(p); + } + + return status; +} + +nsAVIFDecoder::DecodeResult AVIFParser::GetImage(AVIFImage& aImage) { + MOZ_ASSERT(mParser); + + // If the AVIF is animated, get next frame and yield if sequence is not done. + if (IsAnimated()) { + aImage.mColorImage = mColorSampleIter->GetNext(); + + if (!aImage.mColorImage) { + return AsVariant(nsAVIFDecoder::NonDecoderResult::NoSamples); + } + + aImage.mFrameNum = mFrameNum++; + int64_t durationMs = aImage.mColorImage->mDuration.ToMilliseconds(); + aImage.mDuration = FrameTimeout::FromRawMilliseconds( + static_cast(std::min(durationMs, INT32_MAX))); + + if (mAlphaSampleIter) { + aImage.mAlphaImage = mAlphaSampleIter->GetNext(); + if (!aImage.mAlphaImage) { + return AsVariant(nsAVIFDecoder::NonDecoderResult::NoSamples); + } + } + + bool hasNext = mColorSampleIter->HasNext(); + if (mAlphaSampleIter && (hasNext != mAlphaSampleIter->HasNext())) { + MOZ_LOG( + sAVIFLog, LogLevel::Warning, + ("[this=%p] The %s sequence ends before frame %d, aborting decode.", + this, hasNext ? "alpha" : "color", mFrameNum)); + return AsVariant(nsAVIFDecoder::NonDecoderResult::NoSamples); + } + if (!hasNext) { + return AsVariant(nsAVIFDecoder::NonDecoderResult::Complete); + } + return AsVariant(nsAVIFDecoder::NonDecoderResult::OutputAvailable); + } + + if (!mInfo.has_primary_item) { + return AsVariant(nsAVIFDecoder::NonDecoderResult::NoSamples); + } + + // If the AVIF is not animated, get the pitm image and return Complete. + Mp4parseAvifImage image = {}; + Mp4parseStatus status = mp4parse_avif_get_image(mParser.get(), &image); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] mp4parse_avif_get_image -> %d; primary_item length: " + "%zu, alpha_item length: %zu", + this, status, image.primary_image.length, image.alpha_image.length)); + if (status != MP4PARSE_STATUS_OK) { + return AsVariant(status); + } + + MOZ_ASSERT(image.primary_image.data); + RefPtr colorImage = + new MediaRawData(image.primary_image.data, image.primary_image.length); + RefPtr alphaImage = nullptr; + + if (image.alpha_image.length) { + alphaImage = + new MediaRawData(image.alpha_image.data, image.alpha_image.length); + } + + aImage.mFrameNum = 0; + aImage.mDuration = FrameTimeout::Forever(); + aImage.mColorImage = colorImage; + aImage.mAlphaImage = alphaImage; + return AsVariant(nsAVIFDecoder::NonDecoderResult::Complete); +} + +AVIFParser::AVIFParser(const Mp4parseIo* aIo) : mIo(aIo) { + MOZ_ASSERT(mIo); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("Create AVIFParser=%p, image.avif.compliance_strictness: %d", this, + StaticPrefs::image_avif_compliance_strictness())); +} + +static Mp4parseStatus CreateSampleIterator( + Mp4parseAvifParser* aParser, ByteStream* aBuffer, uint32_t trackID, + UniquePtr& aIteratorOut) { + Mp4parseByteData data; + uint64_t timescale; + Mp4parseStatus rv = + mp4parse_avif_get_indice_table(aParser, trackID, &data, ×cale); + if (rv != MP4PARSE_STATUS_OK) { + return rv; + } + + UniquePtr wrapper = MakeUnique(data); + RefPtr index = new MP4SampleIndex( + *wrapper, aBuffer, trackID, false, AssertedCast(timescale)); + aIteratorOut = MakeUnique(index); + return MP4PARSE_STATUS_OK; +} + +Mp4parseStatus AVIFParser::Init(ByteStream* aBuffer, bool aAllowSequences, + bool aAnimateAVIFMajor) { +#define CHECK_MP4PARSE_STATUS(v) \ + do { \ + if ((v) != MP4PARSE_STATUS_OK) { \ + return v; \ + } \ + } while (false) + + MOZ_ASSERT(!mParser); + + Mp4parseAvifParser* parser = nullptr; + Mp4parseStatus status = + mp4parse_avif_new(mIo, + static_cast( + StaticPrefs::image_avif_compliance_strictness()), + &parser); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] mp4parse_avif_new status: %d", this, status)); + CHECK_MP4PARSE_STATUS(status); + MOZ_ASSERT(parser); + mParser.reset(parser); + + status = mp4parse_avif_get_info(mParser.get(), &mInfo); + CHECK_MP4PARSE_STATUS(status); + + bool useSequence = mInfo.has_sequence; + if (useSequence) { + if (!aAllowSequences) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] AVIF sequences disabled", this)); + useSequence = false; + } else if (!aAnimateAVIFMajor && + !!memcmp(mInfo.major_brand, "avis", sizeof(mInfo.major_brand))) { + useSequence = false; + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] AVIF prefers still image", this)); + } + } + + if (useSequence) { + status = CreateSampleIterator(parser, aBuffer, mInfo.color_track_id, + mColorSampleIter); + CHECK_MP4PARSE_STATUS(status); + MOZ_ASSERT(mColorSampleIter); + + if (mInfo.alpha_track_id) { + status = CreateSampleIterator(parser, aBuffer, mInfo.alpha_track_id, + mAlphaSampleIter); + CHECK_MP4PARSE_STATUS(status); + MOZ_ASSERT(mAlphaSampleIter); + } + } + + return status; +} + +bool AVIFParser::IsAnimated() const { return !!mColorSampleIter; } + +// The gfx::YUVColorSpace value is only used in the conversion from YUV -> RGB. +// Typically this comes directly from the CICP matrix_coefficients value, but +// certain values require additionally considering the colour_primaries value. +// See `gfxUtils::CicpToColorSpace` for details. We return a gfx::YUVColorSpace +// rather than CICP::MatrixCoefficients, since that's what +// `gfx::ConvertYCbCrATo[A]RGB` uses. `aBitstreamColorSpaceFunc` abstracts the +// fact that different decoder libraries require different methods for +// extracting the CICP values from the AV1 bitstream and we don't want to do +// that work unnecessarily because in addition to wasted effort, it would make +// the logging more confusing. +template +static gfx::YUVColorSpace GetAVIFColorSpace( + const Mp4parseNclxColourInformation* aNclx, F&& aBitstreamColorSpaceFunc) { + return ToMaybe(aNclx) + .map([=](const auto& nclx) { + return gfxUtils::CicpToColorSpace( + static_cast(nclx.matrix_coefficients), + static_cast(nclx.colour_primaries), + sAVIFLog); + }) + .valueOrFrom(aBitstreamColorSpaceFunc) + .valueOr(gfx::YUVColorSpace::BT601); +} + +static gfx::ColorRange GetAVIFColorRange( + const Mp4parseNclxColourInformation* aNclx, + const gfx::ColorRange av1ColorRange) { + return ToMaybe(aNclx) + .map([=](const auto& nclx) { + return aNclx->full_range_flag ? gfx::ColorRange::FULL + : gfx::ColorRange::LIMITED; + }) + .valueOr(av1ColorRange); +} + +void AVIFDecodedData::SetCicpValues( + const Mp4parseNclxColourInformation* aNclx, + const gfx::CICP::ColourPrimaries aAv1ColourPrimaries, + const gfx::CICP::TransferCharacteristics aAv1TransferCharacteristics, + const gfx::CICP::MatrixCoefficients aAv1MatrixCoefficients) { + auto cp = CICP::ColourPrimaries::CP_UNSPECIFIED; + auto tc = CICP::TransferCharacteristics::TC_UNSPECIFIED; + auto mc = CICP::MatrixCoefficients::MC_UNSPECIFIED; + + if (aNclx) { + cp = static_cast(aNclx->colour_primaries); + tc = static_cast( + aNclx->transfer_characteristics); + mc = static_cast(aNclx->matrix_coefficients); + } + + if (cp == CICP::ColourPrimaries::CP_UNSPECIFIED) { + if (aAv1ColourPrimaries != CICP::ColourPrimaries::CP_UNSPECIFIED) { + cp = aAv1ColourPrimaries; + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("Unspecified colour_primaries value specified in colr box, " + "using AV1 sequence header (%hhu)", + cp)); + } else { + cp = CICP::ColourPrimaries::CP_BT709; + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("Unspecified colour_primaries value specified in colr box " + "or AV1 sequence header, using fallback value (%hhu)", + cp)); + } + } else if (cp != aAv1ColourPrimaries) { + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("colour_primaries mismatch: colr box = %hhu, AV1 " + "sequence header = %hhu, using colr box", + cp, aAv1ColourPrimaries)); + } + + if (tc == CICP::TransferCharacteristics::TC_UNSPECIFIED) { + if (aAv1TransferCharacteristics != + CICP::TransferCharacteristics::TC_UNSPECIFIED) { + tc = aAv1TransferCharacteristics; + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("Unspecified transfer_characteristics value specified in " + "colr box, using AV1 sequence header (%hhu)", + tc)); + } else { + tc = CICP::TransferCharacteristics::TC_SRGB; + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("Unspecified transfer_characteristics value specified in " + "colr box or AV1 sequence header, using fallback value (%hhu)", + tc)); + } + } else if (tc != aAv1TransferCharacteristics) { + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("transfer_characteristics mismatch: colr box = %hhu, " + "AV1 sequence header = %hhu, using colr box", + tc, aAv1TransferCharacteristics)); + } + + if (mc == CICP::MatrixCoefficients::MC_UNSPECIFIED) { + if (aAv1MatrixCoefficients != CICP::MatrixCoefficients::MC_UNSPECIFIED) { + mc = aAv1MatrixCoefficients; + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("Unspecified matrix_coefficients value specified in " + "colr box, using AV1 sequence header (%hhu)", + mc)); + } else { + mc = CICP::MatrixCoefficients::MC_BT601; + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("Unspecified matrix_coefficients value specified in " + "colr box or AV1 sequence header, using fallback value (%hhu)", + mc)); + } + } else if (mc != aAv1MatrixCoefficients) { + MOZ_LOG(sAVIFLog, LogLevel::Warning, + ("matrix_coefficients mismatch: colr box = %hhu, " + "AV1 sequence header = %hhu, using colr box", + mc, aAv1TransferCharacteristics)); + } + + mColourPrimaries = cp; + mTransferCharacteristics = tc; + mMatrixCoefficients = mc; +} + +class Dav1dDecoder final : AVIFDecoderInterface { + public: + ~Dav1dDecoder() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Destroy Dav1dDecoder=%p", this)); + + if (mColorContext) { + dav1d_close(&mColorContext); + MOZ_ASSERT(!mColorContext); + } + + if (mAlphaContext) { + dav1d_close(&mAlphaContext); + MOZ_ASSERT(!mAlphaContext); + } + } + + static DecodeResult Create(UniquePtr& aDecoder, + bool aHasAlpha) { + UniquePtr d(new Dav1dDecoder()); + Dav1dResult r = d->Init(aHasAlpha); + if (r == 0) { + aDecoder.reset(d.release()); + } + return AsVariant(r); + } + + DecodeResult Decode(bool aShouldSendTelemetry, + const Mp4parseAvifInfo& aAVIFInfo, + const AVIFImage& aSamples) override { + MOZ_ASSERT(mColorContext); + MOZ_ASSERT(!mDecodedData); + MOZ_ASSERT(aSamples.mColorImage); + + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("[this=%p] Decoding color", this)); + + OwnedDav1dPicture colorPic = OwnedDav1dPicture(new Dav1dPicture()); + OwnedDav1dPicture alphaPic = nullptr; + Dav1dResult r = GetPicture(*mColorContext, *aSamples.mColorImage, + colorPic.get(), aShouldSendTelemetry); + if (r != 0) { + return AsVariant(r); + } + + if (aSamples.mAlphaImage) { + MOZ_ASSERT(mAlphaContext); + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("[this=%p] Decoding alpha", this)); + + alphaPic = OwnedDav1dPicture(new Dav1dPicture()); + r = GetPicture(*mAlphaContext, *aSamples.mAlphaImage, alphaPic.get(), + aShouldSendTelemetry); + if (r != 0) { + return AsVariant(r); + } + + // Per § 4 of the AVIF spec + // https://aomediacodec.github.io/av1-avif/#auxiliary-images: An AV1 + // Alpha Image Item […] shall be encoded with the same bit depth as the + // associated master AV1 Image Item + if (colorPic->p.bpc != alphaPic->p.bpc) { + return AsVariant(NonDecoderResult::AlphaYColorDepthMismatch); + } + + if (colorPic->stride[0] != alphaPic->stride[0]) { + return AsVariant(NonDecoderResult::AlphaYSizeMismatch); + } + } + + MOZ_ASSERT_IF(!alphaPic, !aAVIFInfo.premultiplied_alpha); + mDecodedData = Dav1dPictureToDecodedData( + aAVIFInfo.nclx_colour_information, std::move(colorPic), + std::move(alphaPic), aAVIFInfo.premultiplied_alpha); + + return AsVariant(r); + } + + private: + explicit Dav1dDecoder() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Create Dav1dDecoder=%p", this)); + } + + Dav1dResult Init(bool aHasAlpha) { + MOZ_ASSERT(!mColorContext); + MOZ_ASSERT(!mAlphaContext); + + Dav1dSettings settings; + dav1d_default_settings(&settings); + settings.all_layers = 0; + settings.max_frame_delay = 1; + // TODO: tune settings a la DAV1DDecoder for AV1 (Bug 1681816) + + Dav1dResult r = dav1d_open(&mColorContext, &settings); + if (r != 0) { + return r; + } + MOZ_ASSERT(mColorContext); + + if (aHasAlpha) { + r = dav1d_open(&mAlphaContext, &settings); + if (r != 0) { + return r; + } + MOZ_ASSERT(mAlphaContext); + } + + return 0; + } + + static Dav1dResult GetPicture(Dav1dContext& aContext, + const MediaRawData& aBytes, + Dav1dPicture* aPicture, + bool aShouldSendTelemetry) { + MOZ_ASSERT(aPicture); + + Dav1dData dav1dData; + Dav1dResult r = dav1d_data_wrap(&dav1dData, aBytes.Data(), aBytes.Size(), + Dav1dFreeCallback_s, nullptr); + + MOZ_LOG( + sAVIFLog, r == 0 ? LogLevel::Verbose : LogLevel::Error, + ("dav1d_data_wrap(%p, %zu) -> %d", dav1dData.data, dav1dData.sz, r)); + + if (r != 0) { + return r; + } + + r = dav1d_send_data(&aContext, &dav1dData); + + MOZ_LOG(sAVIFLog, r == 0 ? LogLevel::Debug : LogLevel::Error, + ("dav1d_send_data -> %d", r)); + + if (r != 0) { + return r; + } + + r = dav1d_get_picture(&aContext, aPicture); + + MOZ_LOG(sAVIFLog, r == 0 ? LogLevel::Debug : LogLevel::Error, + ("dav1d_get_picture -> %d", r)); + + // We already have the AVIF_DECODE_RESULT histogram to record all the + // successful calls, so only bother recording what type of errors we see + // via events. Unlike AOM, dav1d returns an int, not an enum, so this is + // the easiest way to see if we're getting unexpected behavior to + // investigate. + if (aShouldSendTelemetry && r != 0) { + // Uncomment once bug 1691156 is fixed + // mozilla::Telemetry::SetEventRecordingEnabled("avif"_ns, true); + + mozilla::Telemetry::RecordEvent( + mozilla::Telemetry::EventID::Avif_Dav1dGetPicture_ReturnValue, + Some(nsPrintfCString("%d", r)), Nothing()); + } + + return r; + } + + // A dummy callback for dav1d_data_wrap + static void Dav1dFreeCallback_s(const uint8_t* aBuf, void* aCookie) { + // The buf is managed by the mParser inside Dav1dDecoder itself. Do + // nothing here. + } + + static UniquePtr Dav1dPictureToDecodedData( + const Mp4parseNclxColourInformation* aNclx, OwnedDav1dPicture aPicture, + OwnedDav1dPicture aAlphaPlane, bool aPremultipliedAlpha); + + Dav1dContext* mColorContext = nullptr; + Dav1dContext* mAlphaContext = nullptr; +}; + +OwnedAOMImage::OwnedAOMImage() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Create OwnedAOMImage=%p", this)); +} + +OwnedAOMImage::~OwnedAOMImage() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Destroy OwnedAOMImage=%p", this)); +} + +bool OwnedAOMImage::CloneFrom(aom_image_t* aImage, bool aIsAlpha) { + MOZ_ASSERT(aImage); + MOZ_ASSERT(!mImage); + MOZ_ASSERT(!mBuffer); + + uint8_t* srcY = aImage->planes[AOM_PLANE_Y]; + int yStride = aImage->stride[AOM_PLANE_Y]; + int yHeight = aom_img_plane_height(aImage, AOM_PLANE_Y); + size_t yBufSize = yStride * yHeight; + + // If aImage is alpha plane. The data is located in Y channel. + if (aIsAlpha) { + mBuffer = MakeUniqueFallible(yBufSize); + if (!mBuffer) { + return false; + } + uint8_t* destY = mBuffer.get(); + memcpy(destY, srcY, yBufSize); + mImage.emplace(*aImage); + mImage->planes[AOM_PLANE_Y] = destY; + + return true; + } + + uint8_t* srcCb = aImage->planes[AOM_PLANE_U]; + int cbStride = aImage->stride[AOM_PLANE_U]; + int cbHeight = aom_img_plane_height(aImage, AOM_PLANE_U); + size_t cbBufSize = cbStride * cbHeight; + + uint8_t* srcCr = aImage->planes[AOM_PLANE_V]; + int crStride = aImage->stride[AOM_PLANE_V]; + int crHeight = aom_img_plane_height(aImage, AOM_PLANE_V); + size_t crBufSize = crStride * crHeight; + + mBuffer = MakeUniqueFallible(yBufSize + cbBufSize + crBufSize); + if (!mBuffer) { + return false; + } + + uint8_t* destY = mBuffer.get(); + uint8_t* destCb = destY + yBufSize; + uint8_t* destCr = destCb + cbBufSize; + + memcpy(destY, srcY, yBufSize); + memcpy(destCb, srcCb, cbBufSize); + memcpy(destCr, srcCr, crBufSize); + + mImage.emplace(*aImage); + mImage->planes[AOM_PLANE_Y] = destY; + mImage->planes[AOM_PLANE_U] = destCb; + mImage->planes[AOM_PLANE_V] = destCr; + + return true; +} + +/* static */ +OwnedAOMImage* OwnedAOMImage::CopyFrom(aom_image_t* aImage, bool aIsAlpha) { + MOZ_ASSERT(aImage); + UniquePtr img(new OwnedAOMImage()); + if (!img->CloneFrom(aImage, aIsAlpha)) { + return nullptr; + } + return img.release(); +} + +class AOMDecoder final : AVIFDecoderInterface { + public: + ~AOMDecoder() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Destroy AOMDecoder=%p", this)); + + if (mColorContext.isSome()) { + aom_codec_err_t r = aom_codec_destroy(mColorContext.ptr()); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] aom_codec_destroy -> %d", this, r)); + } + + if (mAlphaContext.isSome()) { + aom_codec_err_t r = aom_codec_destroy(mAlphaContext.ptr()); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] aom_codec_destroy -> %d", this, r)); + } + } + + static DecodeResult Create(UniquePtr& aDecoder, + bool aHasAlpha) { + UniquePtr d(new AOMDecoder()); + aom_codec_err_t e = d->Init(aHasAlpha); + if (e == AOM_CODEC_OK) { + aDecoder.reset(d.release()); + } + return AsVariant(AOMResult(e)); + } + + DecodeResult Decode(bool aShouldSendTelemetry, + const Mp4parseAvifInfo& aAVIFInfo, + const AVIFImage& aSamples) override { + MOZ_ASSERT(mColorContext.isSome()); + MOZ_ASSERT(!mDecodedData); + MOZ_ASSERT(aSamples.mColorImage); + + aom_image_t* aomImg = nullptr; + DecodeResult r = GetImage(*mColorContext, *aSamples.mColorImage, &aomImg, + aShouldSendTelemetry); + if (!IsDecodeSuccess(r)) { + return r; + } + MOZ_ASSERT(aomImg); + + // The aomImg will be released in next GetImage call (aom_codec_decode + // actually). The GetImage could be called again immediately if parsedImg + // contains alpha data. Therefore, we need to copy the image and manage it + // by AOMDecoder itself. + OwnedAOMImage* clonedImg = OwnedAOMImage::CopyFrom(aomImg, false); + if (!clonedImg) { + return AsVariant(NonDecoderResult::OutOfMemory); + } + mOwnedImage.reset(clonedImg); + + if (aSamples.mAlphaImage) { + MOZ_ASSERT(mAlphaContext.isSome()); + + aom_image_t* alphaImg = nullptr; + r = GetImage(*mAlphaContext, *aSamples.mAlphaImage, &alphaImg, + aShouldSendTelemetry); + if (!IsDecodeSuccess(r)) { + return r; + } + MOZ_ASSERT(alphaImg); + + OwnedAOMImage* clonedAlphaImg = OwnedAOMImage::CopyFrom(alphaImg, true); + if (!clonedAlphaImg) { + return AsVariant(NonDecoderResult::OutOfMemory); + } + mOwnedAlphaPlane.reset(clonedAlphaImg); + + // Per § 4 of the AVIF spec + // https://aomediacodec.github.io/av1-avif/#auxiliary-images: An AV1 + // Alpha Image Item […] shall be encoded with the same bit depth as the + // associated master AV1 Image Item + MOZ_ASSERT(mOwnedImage->GetImage() && mOwnedAlphaPlane->GetImage()); + if (mOwnedImage->GetImage()->bit_depth != + mOwnedAlphaPlane->GetImage()->bit_depth) { + return AsVariant(NonDecoderResult::AlphaYColorDepthMismatch); + } + + if (mOwnedImage->GetImage()->stride[AOM_PLANE_Y] != + mOwnedAlphaPlane->GetImage()->stride[AOM_PLANE_Y]) { + return AsVariant(NonDecoderResult::AlphaYSizeMismatch); + } + } + + MOZ_ASSERT_IF(!mOwnedAlphaPlane, !aAVIFInfo.premultiplied_alpha); + mDecodedData = AOMImageToToDecodedData( + aAVIFInfo.nclx_colour_information, std::move(mOwnedImage), + std::move(mOwnedAlphaPlane), aAVIFInfo.premultiplied_alpha); + + return r; + } + + private: + explicit AOMDecoder() { + MOZ_LOG(sAVIFLog, LogLevel::Verbose, ("Create AOMDecoder=%p", this)); + } + + aom_codec_err_t Init(bool aHasAlpha) { + MOZ_ASSERT(mColorContext.isNothing()); + MOZ_ASSERT(mAlphaContext.isNothing()); + + aom_codec_iface_t* iface = aom_codec_av1_dx(); + + // Init color decoder context + mColorContext.emplace(); + aom_codec_err_t r = aom_codec_dec_init( + mColorContext.ptr(), iface, /* cfg = */ nullptr, /* flags = */ 0); + + MOZ_LOG(sAVIFLog, r == AOM_CODEC_OK ? LogLevel::Verbose : LogLevel::Error, + ("[this=%p] color decoder: aom_codec_dec_init -> %d, name = %s", + this, r, mColorContext->name)); + + if (r != AOM_CODEC_OK) { + mColorContext.reset(); + return r; + } + + if (aHasAlpha) { + // Init alpha decoder context + mAlphaContext.emplace(); + r = aom_codec_dec_init(mAlphaContext.ptr(), iface, /* cfg = */ nullptr, + /* flags = */ 0); + + MOZ_LOG(sAVIFLog, r == AOM_CODEC_OK ? LogLevel::Verbose : LogLevel::Error, + ("[this=%p] color decoder: aom_codec_dec_init -> %d, name = %s", + this, r, mAlphaContext->name)); + + if (r != AOM_CODEC_OK) { + mAlphaContext.reset(); + return r; + } + } + + return r; + } + + static DecodeResult GetImage(aom_codec_ctx_t& aContext, + const MediaRawData& aData, aom_image_t** aImage, + bool aShouldSendTelemetry) { + aom_codec_err_t r = + aom_codec_decode(&aContext, aData.Data(), aData.Size(), nullptr); + + MOZ_LOG(sAVIFLog, r == AOM_CODEC_OK ? LogLevel::Verbose : LogLevel::Error, + ("aom_codec_decode -> %d", r)); + + if (aShouldSendTelemetry) { + switch (r) { + case AOM_CODEC_OK: + // No need to record any telemetry for the common case + break; + case AOM_CODEC_ERROR: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::error); + break; + case AOM_CODEC_MEM_ERROR: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::mem_error); + break; + case AOM_CODEC_ABI_MISMATCH: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::abi_mismatch); + break; + case AOM_CODEC_INCAPABLE: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::incapable); + break; + case AOM_CODEC_UNSUP_BITSTREAM: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::unsup_bitstream); + break; + case AOM_CODEC_UNSUP_FEATURE: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::unsup_feature); + break; + case AOM_CODEC_CORRUPT_FRAME: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::corrupt_frame); + break; + case AOM_CODEC_INVALID_PARAM: + AccumulateCategorical(LABELS_AVIF_AOM_DECODE_ERROR::invalid_param); + break; + default: + MOZ_ASSERT_UNREACHABLE( + "Unknown aom_codec_err_t value from aom_codec_decode"); + } + } + + if (r != AOM_CODEC_OK) { + return AsVariant(AOMResult(r)); + } + + aom_codec_iter_t iter = nullptr; + aom_image_t* img = aom_codec_get_frame(&aContext, &iter); + + MOZ_LOG(sAVIFLog, img == nullptr ? LogLevel::Error : LogLevel::Verbose, + ("aom_codec_get_frame -> %p", img)); + + if (img == nullptr) { + return AsVariant(AOMResult(NonAOMCodecError::NoFrame)); + } + + const CheckedInt decoded_width = img->d_w; + const CheckedInt decoded_height = img->d_h; + + if (!decoded_height.isValid() || !decoded_width.isValid()) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("image dimensions can't be stored in int: d_w: %u, " + "d_h: %u", + img->d_w, img->d_h)); + return AsVariant(AOMResult(NonAOMCodecError::SizeOverflow)); + } + + *aImage = img; + return AsVariant(AOMResult(r)); + } + + static UniquePtr AOMImageToToDecodedData( + const Mp4parseNclxColourInformation* aNclx, + UniquePtr aImage, UniquePtr aAlphaPlane, + bool aPremultipliedAlpha); + + Maybe mColorContext; + Maybe mAlphaContext; + UniquePtr mOwnedImage; + UniquePtr mOwnedAlphaPlane; +}; + +/* static */ +UniquePtr Dav1dDecoder::Dav1dPictureToDecodedData( + const Mp4parseNclxColourInformation* aNclx, OwnedDav1dPicture aPicture, + OwnedDav1dPicture aAlphaPlane, bool aPremultipliedAlpha) { + MOZ_ASSERT(aPicture); + + static_assert(std::is_samep.w)>::value); + static_assert(std::is_samep.h)>::value); + + UniquePtr data = MakeUnique(); + + data->mRenderSize.emplace(aPicture->frame_hdr->render_width, + aPicture->frame_hdr->render_height); + + data->mYChannel = static_cast(aPicture->data[0]); + data->mYStride = aPicture->stride[0]; + data->mYSkip = aPicture->stride[0] - aPicture->p.w; + data->mCbChannel = static_cast(aPicture->data[1]); + data->mCrChannel = static_cast(aPicture->data[2]); + data->mCbCrStride = aPicture->stride[1]; + + switch (aPicture->p.layout) { + case DAV1D_PIXEL_LAYOUT_I400: // Monochrome, so no Cb or Cr channels + break; + case DAV1D_PIXEL_LAYOUT_I420: + data->mChromaSubsampling = ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; + break; + case DAV1D_PIXEL_LAYOUT_I422: + data->mChromaSubsampling = ChromaSubsampling::HALF_WIDTH; + break; + case DAV1D_PIXEL_LAYOUT_I444: + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown pixel layout"); + } + + data->mCbSkip = aPicture->stride[1] - aPicture->p.w; + data->mCrSkip = aPicture->stride[1] - aPicture->p.w; + data->mPictureRect = IntRect(0, 0, aPicture->p.w, aPicture->p.h); + data->mStereoMode = StereoMode::MONO; + data->mColorDepth = ColorDepthForBitDepth(aPicture->p.bpc); + + MOZ_ASSERT(aPicture->p.bpc == BitDepthForColorDepth(data->mColorDepth)); + + data->mYUVColorSpace = GetAVIFColorSpace(aNclx, [&]() { + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("YUVColorSpace cannot be determined from colr box, using AV1 " + "sequence header")); + return DAV1DDecoder::GetColorSpace(*aPicture, sAVIFLog); + }); + + auto av1ColourPrimaries = CICP::ColourPrimaries::CP_UNSPECIFIED; + auto av1TransferCharacteristics = + CICP::TransferCharacteristics::TC_UNSPECIFIED; + auto av1MatrixCoefficients = CICP::MatrixCoefficients::MC_UNSPECIFIED; + + MOZ_ASSERT(aPicture->seq_hdr); + auto& seq_hdr = *aPicture->seq_hdr; + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("seq_hdr.color_description_present: %d", + seq_hdr.color_description_present)); + if (seq_hdr.color_description_present) { + av1ColourPrimaries = static_cast(seq_hdr.pri); + av1TransferCharacteristics = + static_cast(seq_hdr.trc); + av1MatrixCoefficients = static_cast(seq_hdr.mtrx); + } + + data->SetCicpValues(aNclx, av1ColourPrimaries, av1TransferCharacteristics, + av1MatrixCoefficients); + + gfx::ColorRange av1ColorRange = + seq_hdr.color_range ? gfx::ColorRange::FULL : gfx::ColorRange::LIMITED; + data->mColorRange = GetAVIFColorRange(aNclx, av1ColorRange); + + auto colorPrimaries = + gfxUtils::CicpToColorPrimaries(data->mColourPrimaries, sAVIFLog); + if (colorPrimaries.isSome()) { + data->mColorPrimaries = *colorPrimaries; + } + + if (aAlphaPlane) { + MOZ_ASSERT(aAlphaPlane->stride[0] == data->mYStride); + data->mAlpha.emplace(); + data->mAlpha->mChannel = static_cast(aAlphaPlane->data[0]); + data->mAlpha->mSize = gfx::IntSize(aAlphaPlane->p.w, aAlphaPlane->p.h); + data->mAlpha->mPremultiplied = aPremultipliedAlpha; + } + + data->mColorDav1d = std::move(aPicture); + data->mAlphaDav1d = std::move(aAlphaPlane); + + return data; +} + +/* static */ +UniquePtr AOMDecoder::AOMImageToToDecodedData( + const Mp4parseNclxColourInformation* aNclx, UniquePtr aImage, + UniquePtr aAlphaPlane, bool aPremultipliedAlpha) { + aom_image_t* colorImage = aImage->GetImage(); + aom_image_t* alphaImage = aAlphaPlane ? aAlphaPlane->GetImage() : nullptr; + + MOZ_ASSERT(colorImage); + MOZ_ASSERT(colorImage->stride[AOM_PLANE_Y] == + colorImage->stride[AOM_PLANE_ALPHA]); + MOZ_ASSERT(colorImage->stride[AOM_PLANE_Y] >= + aom_img_plane_width(colorImage, AOM_PLANE_Y)); + MOZ_ASSERT(colorImage->stride[AOM_PLANE_U] == + colorImage->stride[AOM_PLANE_V]); + MOZ_ASSERT(colorImage->stride[AOM_PLANE_U] >= + aom_img_plane_width(colorImage, AOM_PLANE_U)); + MOZ_ASSERT(colorImage->stride[AOM_PLANE_V] >= + aom_img_plane_width(colorImage, AOM_PLANE_V)); + MOZ_ASSERT(aom_img_plane_width(colorImage, AOM_PLANE_U) == + aom_img_plane_width(colorImage, AOM_PLANE_V)); + MOZ_ASSERT(aom_img_plane_height(colorImage, AOM_PLANE_U) == + aom_img_plane_height(colorImage, AOM_PLANE_V)); + + UniquePtr data = MakeUnique(); + + data->mRenderSize.emplace(colorImage->r_w, colorImage->r_h); + + data->mYChannel = colorImage->planes[AOM_PLANE_Y]; + data->mYStride = colorImage->stride[AOM_PLANE_Y]; + data->mYSkip = colorImage->stride[AOM_PLANE_Y] - + aom_img_plane_width(colorImage, AOM_PLANE_Y); + data->mCbChannel = colorImage->planes[AOM_PLANE_U]; + data->mCrChannel = colorImage->planes[AOM_PLANE_V]; + data->mCbCrStride = colorImage->stride[AOM_PLANE_U]; + data->mCbSkip = colorImage->stride[AOM_PLANE_U] - + aom_img_plane_width(colorImage, AOM_PLANE_U); + data->mCrSkip = colorImage->stride[AOM_PLANE_V] - + aom_img_plane_width(colorImage, AOM_PLANE_V); + data->mPictureRect = gfx::IntRect(0, 0, colorImage->d_w, colorImage->d_h); + data->mStereoMode = StereoMode::MONO; + data->mColorDepth = ColorDepthForBitDepth(colorImage->bit_depth); + + if (colorImage->x_chroma_shift == 1 && colorImage->y_chroma_shift == 1) { + data->mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; + } else if (colorImage->x_chroma_shift == 1 && + colorImage->y_chroma_shift == 0) { + data->mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH; + } else if (colorImage->x_chroma_shift != 0 || + colorImage->y_chroma_shift != 0) { + MOZ_ASSERT_UNREACHABLE("unexpected chroma shifts"); + } + + MOZ_ASSERT(colorImage->bit_depth == BitDepthForColorDepth(data->mColorDepth)); + + auto av1ColourPrimaries = static_cast(colorImage->cp); + auto av1TransferCharacteristics = + static_cast(colorImage->tc); + auto av1MatrixCoefficients = + static_cast(colorImage->mc); + + data->mYUVColorSpace = GetAVIFColorSpace(aNclx, [=]() { + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("YUVColorSpace cannot be determined from colr box, using AV1 " + "sequence header")); + return gfxUtils::CicpToColorSpace(av1MatrixCoefficients, av1ColourPrimaries, + sAVIFLog); + }); + + gfx::ColorRange av1ColorRange; + if (colorImage->range == AOM_CR_STUDIO_RANGE) { + av1ColorRange = gfx::ColorRange::LIMITED; + } else { + MOZ_ASSERT(colorImage->range == AOM_CR_FULL_RANGE); + av1ColorRange = gfx::ColorRange::FULL; + } + data->mColorRange = GetAVIFColorRange(aNclx, av1ColorRange); + + data->SetCicpValues(aNclx, av1ColourPrimaries, av1TransferCharacteristics, + av1MatrixCoefficients); + + auto colorPrimaries = + gfxUtils::CicpToColorPrimaries(data->mColourPrimaries, sAVIFLog); + if (colorPrimaries.isSome()) { + data->mColorPrimaries = *colorPrimaries; + } + + if (alphaImage) { + MOZ_ASSERT(alphaImage->stride[AOM_PLANE_Y] == data->mYStride); + data->mAlpha.emplace(); + data->mAlpha->mChannel = alphaImage->planes[AOM_PLANE_Y]; + data->mAlpha->mSize = gfx::IntSize(alphaImage->d_w, alphaImage->d_h); + data->mAlpha->mPremultiplied = aPremultipliedAlpha; + } + + data->mColorAOM = std::move(aImage); + data->mAlphaAOM = std::move(aAlphaPlane); + + return data; +} + +// Wrapper to allow rust to call our read adaptor. +intptr_t nsAVIFDecoder::ReadSource(uint8_t* aDestBuf, uintptr_t aDestBufSize, + void* aUserData) { + MOZ_ASSERT(aDestBuf); + MOZ_ASSERT(aUserData); + + MOZ_LOG(sAVIFLog, LogLevel::Verbose, + ("AVIF ReadSource, aDestBufSize: %zu", aDestBufSize)); + + auto* decoder = reinterpret_cast(aUserData); + + MOZ_ASSERT(decoder->mReadCursor); + + size_t bufferLength = decoder->mBufferedData.end() - decoder->mReadCursor; + size_t n_bytes = std::min(aDestBufSize, bufferLength); + + MOZ_LOG( + sAVIFLog, LogLevel::Verbose, + ("AVIF ReadSource, %zu bytes ready, copying %zu", bufferLength, n_bytes)); + + memcpy(aDestBuf, decoder->mReadCursor, n_bytes); + decoder->mReadCursor += n_bytes; + + return n_bytes; +} + +nsAVIFDecoder::nsAVIFDecoder(RasterImage* aImage) : Decoder(aImage) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] nsAVIFDecoder::nsAVIFDecoder", this)); +} + +nsAVIFDecoder::~nsAVIFDecoder() { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] nsAVIFDecoder::~nsAVIFDecoder", this)); +} + +LexerResult nsAVIFDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("[this=%p] nsAVIFDecoder::DoDecode start", this)); + + DecodeResult result = DoDecodeInternal(aIterator, aOnResume); + + RecordDecodeResultTelemetry(result); + + if (result.is()) { + NonDecoderResult r = result.as(); + if (r == NonDecoderResult::NeedMoreData) { + return LexerResult(Yield::NEED_MORE_DATA); + } + if (r == NonDecoderResult::OutputAvailable) { + MOZ_ASSERT(HasSize()); + return LexerResult(Yield::OUTPUT_AVAILABLE); + } + if (r == NonDecoderResult::Complete) { + MOZ_ASSERT(HasSize()); + return LexerResult(TerminalState::SUCCESS); + } + return LexerResult(TerminalState::FAILURE); + } + + MOZ_ASSERT(result.is() || result.is() || + result.is()); + // If IsMetadataDecode(), a successful parse should return + // NonDecoderResult::MetadataOk or else continue to the decode stage + MOZ_ASSERT_IF(result.is(), + result.as() != MP4PARSE_STATUS_OK); + auto rv = LexerResult(IsDecodeSuccess(result) ? TerminalState::SUCCESS + : TerminalState::FAILURE); + MOZ_LOG(sAVIFLog, LogLevel::Info, + ("[this=%p] nsAVIFDecoder::DoDecode end", this)); + return rv; +} + +Mp4parseStatus nsAVIFDecoder::CreateParser() { + if (!mParser) { + Mp4parseIo io = {nsAVIFDecoder::ReadSource, this}; + mBufferStream = new AVIFDecoderStream(&mBufferedData); + + Mp4parseStatus status = AVIFParser::Create( + &io, mBufferStream.get(), mParser, + bool(GetDecoderFlags() & DecoderFlags::AVIF_SEQUENCES_ENABLED), + bool(GetDecoderFlags() & DecoderFlags::AVIF_ANIMATE_AVIF_MAJOR)); + + if (status != MP4PARSE_STATUS_OK) { + return status; + } + + const Mp4parseAvifInfo& info = mParser->GetInfo(); + mIsAnimated = mParser->IsAnimated(); + mHasAlpha = mIsAnimated ? !!info.alpha_track_id : info.has_alpha_item; + } + + return MP4PARSE_STATUS_OK; +} + +nsAVIFDecoder::DecodeResult nsAVIFDecoder::CreateDecoder() { + if (!mDecoder) { + DecodeResult r = StaticPrefs::image_avif_use_dav1d() + ? Dav1dDecoder::Create(mDecoder, mHasAlpha) + : AOMDecoder::Create(mDecoder, mHasAlpha); + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] Create %sDecoder %ssuccessfully", this, + StaticPrefs::image_avif_use_dav1d() ? "Dav1d" : "AOM", + IsDecodeSuccess(r) ? "" : "un")); + + return r; + } + + return StaticPrefs::image_avif_use_dav1d() + ? DecodeResult(Dav1dResult(0)) + : DecodeResult(AOMResult(AOM_CODEC_OK)); +} + +// Records all telemetry available in the AVIF metadata, called only once during +// the metadata decode to avoid multiple counts. +static void RecordMetadataTelem(const Mp4parseAvifInfo& aInfo) { + if (aInfo.pixel_aspect_ratio) { + const uint32_t& h_spacing = aInfo.pixel_aspect_ratio->h_spacing; + const uint32_t& v_spacing = aInfo.pixel_aspect_ratio->v_spacing; + + if (h_spacing == 0 || v_spacing == 0) { + AccumulateCategorical(LABELS_AVIF_PASP::invalid); + } else if (h_spacing == v_spacing) { + AccumulateCategorical(LABELS_AVIF_PASP::square); + } else { + AccumulateCategorical(LABELS_AVIF_PASP::nonsquare); + } + } else { + AccumulateCategorical(LABELS_AVIF_PASP::absent); + } + + const auto& major_brand = aInfo.major_brand; + if (!memcmp(major_brand, "avif", sizeof(major_brand))) { + AccumulateCategorical(LABELS_AVIF_MAJOR_BRAND::avif); + } else if (!memcmp(major_brand, "avis", sizeof(major_brand))) { + AccumulateCategorical(LABELS_AVIF_MAJOR_BRAND::avis); + } else { + AccumulateCategorical(LABELS_AVIF_MAJOR_BRAND::other); + } + + AccumulateCategorical(aInfo.has_sequence ? LABELS_AVIF_SEQUENCE::present + : LABELS_AVIF_SEQUENCE::absent); + +#define FEATURE_TELEMETRY(fourcc) \ + AccumulateCategorical( \ + (aInfo.unsupported_features_bitfield & (1 << MP4PARSE_FEATURE_##fourcc)) \ + ? LABELS_AVIF_##fourcc::present \ + : LABELS_AVIF_##fourcc::absent) + FEATURE_TELEMETRY(A1LX); + FEATURE_TELEMETRY(A1OP); + FEATURE_TELEMETRY(CLAP); + FEATURE_TELEMETRY(GRID); + FEATURE_TELEMETRY(IPRO); + FEATURE_TELEMETRY(LSEL); + + if (aInfo.nclx_colour_information && aInfo.icc_colour_information.data) { + AccumulateCategorical(LABELS_AVIF_COLR::both); + } else if (aInfo.nclx_colour_information) { + AccumulateCategorical(LABELS_AVIF_COLR::nclx); + } else if (aInfo.icc_colour_information.data) { + AccumulateCategorical(LABELS_AVIF_COLR::icc); + } else { + AccumulateCategorical(LABELS_AVIF_COLR::absent); + } +} + +static void RecordPixiTelemetry(uint8_t aPixiBitDepth, + uint8_t aBitstreamBitDepth, + const char* aItemName) { + if (aPixiBitDepth == 0) { + AccumulateCategorical(LABELS_AVIF_PIXI::absent); + } else if (aPixiBitDepth == aBitstreamBitDepth) { + AccumulateCategorical(LABELS_AVIF_PIXI::valid); + } else { + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("%s item pixi bit depth (%hhu) doesn't match " + "bitstream (%hhu)", + aItemName, aPixiBitDepth, aBitstreamBitDepth)); + AccumulateCategorical(LABELS_AVIF_PIXI::bitstream_mismatch); + } +} + +// This telemetry depends on the results of decoding. +// These data must be recorded only on the first frame decoded after metadata +// decode finishes. +static void RecordFrameTelem(bool aAnimated, const Mp4parseAvifInfo& aInfo, + const AVIFDecodedData& aData) { + AccumulateCategorical( + gColorSpaceLabel[static_cast(aData.mYUVColorSpace)]); + AccumulateCategorical( + gColorDepthLabel[static_cast(aData.mColorDepth)]); + + RecordPixiTelemetry( + aAnimated ? aInfo.color_track_bit_depth : aInfo.primary_item_bit_depth, + BitDepthForColorDepth(aData.mColorDepth), "color"); + + if (aData.mAlpha) { + AccumulateCategorical(LABELS_AVIF_ALPHA::present); + RecordPixiTelemetry( + aAnimated ? aInfo.alpha_track_bit_depth : aInfo.alpha_item_bit_depth, + BitDepthForColorDepth(aData.mColorDepth), "alpha"); + } else { + AccumulateCategorical(LABELS_AVIF_ALPHA::absent); + } + + if (CICP::IsReserved(aData.mColourPrimaries)) { + AccumulateCategorical(LABELS_AVIF_CICP_CP::RESERVED_REST); + } else { + AccumulateCategorical( + static_cast(aData.mColourPrimaries)); + } + + if (CICP::IsReserved(aData.mTransferCharacteristics)) { + AccumulateCategorical(LABELS_AVIF_CICP_TC::RESERVED); + } else { + AccumulateCategorical( + static_cast(aData.mTransferCharacteristics)); + } + + if (CICP::IsReserved(aData.mMatrixCoefficients)) { + AccumulateCategorical(LABELS_AVIF_CICP_MC::RESERVED); + } else { + AccumulateCategorical( + static_cast(aData.mMatrixCoefficients)); + } +} + +nsAVIFDecoder::DecodeResult nsAVIFDecoder::DoDecodeInternal( + SourceBufferIterator& aIterator, IResumable* aOnResume) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] nsAVIFDecoder::DoDecodeInternal", this)); + + // Since the SourceBufferIterator doesn't guarantee a contiguous buffer, + // but the current mp4parse-rust implementation requires it, always buffer + // locally. This keeps the code simpler at the cost of some performance, but + // this implementation is only experimental, so we don't want to spend time + // optimizing it prematurely. + while (!mReadCursor) { + SourceBufferIterator::State state = + aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume); + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] After advance, iterator state is %d", this, state)); + + switch (state) { + case SourceBufferIterator::WAITING: + return AsVariant(NonDecoderResult::NeedMoreData); + + case SourceBufferIterator::COMPLETE: + mReadCursor = mBufferedData.begin(); + break; + + case SourceBufferIterator::READY: { // copy new data to buffer + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] SourceBufferIterator ready, %zu bytes available", + this, aIterator.Length())); + + bool appendSuccess = + mBufferedData.append(aIterator.Data(), aIterator.Length()); + + if (!appendSuccess) { + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("[this=%p] Failed to append %zu bytes to buffer", this, + aIterator.Length())); + } + + break; + } + + default: + MOZ_ASSERT_UNREACHABLE("unexpected SourceBufferIterator state"); + } + } + + Mp4parseStatus parserStatus = CreateParser(); + + if (parserStatus != MP4PARSE_STATUS_OK) { + return AsVariant(parserStatus); + } + + const Mp4parseAvifInfo& parsedInfo = mParser->GetInfo(); + + if (parsedInfo.icc_colour_information.data) { + const auto& icc = parsedInfo.icc_colour_information; + MOZ_LOG( + sAVIFLog, LogLevel::Debug, + ("[this=%p] colr type ICC: %zu bytes %p", this, icc.length, icc.data)); + } + + if (IsMetadataDecode()) { + RecordMetadataTelem(parsedInfo); + } + + if (parsedInfo.nclx_colour_information) { + const auto& nclx = *parsedInfo.nclx_colour_information; + MOZ_LOG( + sAVIFLog, LogLevel::Debug, + ("[this=%p] colr type CICP: cp/tc/mc/full-range %u/%u/%u/%s", this, + nclx.colour_primaries, nclx.transfer_characteristics, + nclx.matrix_coefficients, nclx.full_range_flag ? "true" : "false")); + } + + if (!parsedInfo.icc_colour_information.data && + !parsedInfo.nclx_colour_information) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] colr box not present", this)); + } + + AVIFImage parsedImage; + DecodeResult r = mParser->GetImage(parsedImage); + if (!IsDecodeSuccess(r)) { + return r; + } + bool isDone = + !IsMetadataDecode() && r == DecodeResult(NonDecoderResult::Complete); + + if (mIsAnimated) { + PostIsAnimated(parsedImage.mDuration); + } + if (mHasAlpha) { + PostHasTransparency(); + } + + Orientation orientation = StaticPrefs::image_avif_apply_transforms() + ? GetImageOrientation(parsedInfo) + : Orientation{}; + // TODO: Orientation should probably also apply to animated AVIFs. + if (mIsAnimated) { + orientation = Orientation{}; + } + + MaybeIntSize ispeImageSize = GetImageSize(parsedInfo); + + bool sendDecodeTelemetry = IsMetadataDecode(); + if (ispeImageSize.isSome()) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] Parser returned image size %d x %d (%d/%d bit)", this, + ispeImageSize->width, ispeImageSize->height, + mIsAnimated ? parsedInfo.color_track_bit_depth + : parsedInfo.primary_item_bit_depth, + mIsAnimated ? parsedInfo.alpha_track_bit_depth + : parsedInfo.alpha_item_bit_depth)); + PostSize(ispeImageSize->width, ispeImageSize->height, orientation); + if (IsMetadataDecode()) { + MOZ_LOG( + sAVIFLog, LogLevel::Debug, + ("[this=%p] Finishing metadata decode without image decode", this)); + return AsVariant(NonDecoderResult::Complete); + } + // If we're continuing to decode here, this means we skipped decode + // telemetry for the metadata decode pass. Send it this time. + sendDecodeTelemetry = true; + } else { + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("[this=%p] Parser returned no image size, decoding...", this)); + } + + r = CreateDecoder(); + if (!IsDecodeSuccess(r)) { + return r; + } + MOZ_ASSERT(mDecoder); + r = mDecoder->Decode(sendDecodeTelemetry, parsedInfo, parsedImage); + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] Decoder%s->Decode() %s", this, + StaticPrefs::image_avif_use_dav1d() ? "Dav1d" : "AOM", + IsDecodeSuccess(r) ? "succeeds" : "fails")); + + if (!IsDecodeSuccess(r)) { + return r; + } + + UniquePtr decodedData = mDecoder->GetDecodedData(); + + MOZ_ASSERT_IF(mHasAlpha, decodedData->mAlpha.isSome()); + + MOZ_ASSERT(decodedData->mColourPrimaries != + CICP::ColourPrimaries::CP_UNSPECIFIED); + MOZ_ASSERT(decodedData->mTransferCharacteristics != + CICP::TransferCharacteristics::TC_UNSPECIFIED); + MOZ_ASSERT(decodedData->mColorRange <= gfx::ColorRange::_Last); + MOZ_ASSERT(decodedData->mYUVColorSpace <= gfx::YUVColorSpace::_Last); + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] decodedData.mColorRange: %hhd", this, + static_cast(decodedData->mColorRange))); + + // Technically it's valid but we don't handle it now (Bug 1682318). + if (decodedData->mAlpha && + decodedData->mAlpha->mSize != decodedData->YDataSize()) { + return AsVariant(NonDecoderResult::AlphaYSizeMismatch); + } + + bool isFirstFrame = GetFrameCount() == 0; + + if (!HasSize()) { + MOZ_ASSERT(isFirstFrame); + MOZ_LOG( + sAVIFLog, LogLevel::Error, + ("[this=%p] Using decoded image size: %d x %d", this, + decodedData->mPictureRect.width, decodedData->mPictureRect.height)); + PostSize(decodedData->mPictureRect.width, decodedData->mPictureRect.height, + orientation); + AccumulateCategorical(LABELS_AVIF_ISPE::absent); + } else { + // Verify that the bitstream hasn't changed the image size compared to + // either the ispe box or the previous frames. + IntSize expectedSize = GetImageMetadata() + .GetOrientation() + .ToUnoriented(Size()) + .ToUnknownSize(); + if (decodedData->mPictureRect.width != expectedSize.width || + decodedData->mPictureRect.height != expectedSize.height) { + if (isFirstFrame) { + MOZ_LOG( + sAVIFLog, LogLevel::Error, + ("[this=%p] Metadata image size doesn't match decoded image size: " + "(%d x %d) != (%d x %d)", + this, ispeImageSize->width, ispeImageSize->height, + decodedData->mPictureRect.width, + decodedData->mPictureRect.height)); + AccumulateCategorical(LABELS_AVIF_ISPE::bitstream_mismatch); + return AsVariant(NonDecoderResult::MetadataImageSizeMismatch); + } + + MOZ_LOG( + sAVIFLog, LogLevel::Error, + ("[this=%p] Frame size has changed in the bitstream: " + "(%d x %d) != (%d x %d)", + this, expectedSize.width, expectedSize.height, + decodedData->mPictureRect.width, decodedData->mPictureRect.height)); + return AsVariant(NonDecoderResult::FrameSizeChanged); + } + + if (isFirstFrame) { + AccumulateCategorical(LABELS_AVIF_ISPE::valid); + } + } + + if (IsMetadataDecode()) { + return AsVariant(NonDecoderResult::Complete); + } + + IntSize rgbSize = decodedData->mPictureRect.Size(); + + if (parsedImage.mFrameNum == 0) { + RecordFrameTelem(mIsAnimated, parsedInfo, *decodedData); + } + + if (decodedData->mRenderSize && + decodedData->mRenderSize->ToUnknownSize() != rgbSize) { + // This may be supported by allowing all metadata decodes to decode a frame + // and get the render size from the bitstream. However it's unlikely to be + // used often. + return AsVariant(NonDecoderResult::RenderSizeMismatch); + } + + // Read color profile + if (mCMSMode != CMSMode::Off) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] Processing color profile", this)); + + // See comment on AVIFDecodedData + if (parsedInfo.icc_colour_information.data) { + // same profile for every frame of image, only create it once + if (!mInProfile) { + const auto& icc = parsedInfo.icc_colour_information; + mInProfile = qcms_profile_from_memory(icc.data, icc.length); + } + } else { + // potentially different profile every frame, destroy the old one + if (mInProfile) { + if (mTransform) { + qcms_transform_release(mTransform); + mTransform = nullptr; + } + qcms_profile_release(mInProfile); + mInProfile = nullptr; + } + + const auto& cp = decodedData->mColourPrimaries; + const auto& tc = decodedData->mTransferCharacteristics; + + if (CICP::IsReserved(cp)) { + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("[this=%p] colour_primaries reserved value (%hhu) is invalid; " + "failing", + this, cp)); + return AsVariant(NonDecoderResult::InvalidCICP); + } + + if (CICP::IsReserved(tc)) { + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("[this=%p] transfer_characteristics reserved value (%hhu) is " + "invalid; failing", + this, tc)); + return AsVariant(NonDecoderResult::InvalidCICP); + } + + MOZ_ASSERT(cp != CICP::ColourPrimaries::CP_UNSPECIFIED && + !CICP::IsReserved(cp)); + MOZ_ASSERT(tc != CICP::TransferCharacteristics::TC_UNSPECIFIED && + !CICP::IsReserved(tc)); + + mInProfile = qcms_profile_create_cicp(cp, tc); + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] mInProfile %p", this, mInProfile)); + } else { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] CMSMode::Off, skipping color profile", this)); + } + + if (mInProfile && GetCMSOutputProfile() && !mTransform) { + auto intent = static_cast(gfxPlatform::GetRenderingIntent()); + qcms_data_type inType; + qcms_data_type outType; + + // If we're not mandating an intent, use the one from the image. + if (gfxPlatform::GetRenderingIntent() == -1) { + intent = qcms_profile_get_rendering_intent(mInProfile); + } + + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + if (profileSpace != icSigGrayData) { + // If the transform happens with SurfacePipe, it will be in RGBA if we + // have an alpha channel, because the swizzle and premultiplication + // happens after color management. Otherwise it will be in BGRA because + // the swizzle happens at the start. + if (mHasAlpha) { + inType = QCMS_DATA_RGBA_8; + outType = QCMS_DATA_RGBA_8; + } else { + inType = gfxPlatform::GetCMSOSRGBAType(); + outType = inType; + } + } else { + if (mHasAlpha) { + inType = QCMS_DATA_GRAYA_8; + outType = gfxPlatform::GetCMSOSRGBAType(); + } else { + inType = QCMS_DATA_GRAY_8; + outType = gfxPlatform::GetCMSOSRGBAType(); + } + } + + mTransform = qcms_transform_create(mInProfile, inType, + GetCMSOutputProfile(), outType, intent); + } + + // Get suggested format and size. Note that GetYCbCrToRGBDestFormatAndSize + // force format to be B8G8R8X8 if it's not. + gfx::SurfaceFormat format = SurfaceFormat::OS_RGBX; + gfx::GetYCbCrToRGBDestFormatAndSize(*decodedData, format, rgbSize); + if (mHasAlpha) { + // We would use libyuv to do the YCbCrA -> ARGB convertion, which only + // works for B8G8R8A8. + format = SurfaceFormat::B8G8R8A8; + } + + const int bytesPerPixel = BytesPerPixel(format); + + const CheckedInt rgbStride = CheckedInt(rgbSize.width) * bytesPerPixel; + const CheckedInt rgbBufLength = rgbStride * rgbSize.height; + + if (!rgbStride.isValid() || !rgbBufLength.isValid()) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] overflow calculating rgbBufLength: rbgSize.width: %d, " + "rgbSize.height: %d, " + "bytesPerPixel: %u", + this, rgbSize.width, rgbSize.height, bytesPerPixel)); + return AsVariant(NonDecoderResult::SizeOverflow); + } + + UniquePtr rgbBuf = + MakeUniqueFallible(rgbBufLength.value()); + if (!rgbBuf) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] allocation of %u-byte rgbBuf failed", this, + rgbBufLength.value())); + return AsVariant(NonDecoderResult::OutOfMemory); + } + + if (decodedData->mAlpha) { + const auto wantPremultiply = + !bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA); + const bool& hasPremultiply = decodedData->mAlpha->mPremultiplied; + + PremultFunc premultOp = nullptr; + if (wantPremultiply && !hasPremultiply) { + premultOp = libyuv::ARGBAttenuate; + } else if (!wantPremultiply && hasPremultiply) { + premultOp = libyuv::ARGBUnattenuate; + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] calling gfx::ConvertYCbCrAToARGB premultOp: %p", this, + premultOp)); + gfx::ConvertYCbCrAToARGB(*decodedData, *decodedData->mAlpha, format, + rgbSize, rgbBuf.get(), rgbStride.value(), + premultOp); + } else { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] calling gfx::ConvertYCbCrToRGB", this)); + gfx::ConvertYCbCrToRGB(*decodedData, format, rgbSize, rgbBuf.get(), + rgbStride.value()); + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] calling SurfacePipeFactory::CreateSurfacePipe", this)); + + Maybe pipe = Nothing(); + + if (mIsAnimated) { + SurfaceFormat outFormat = + decodedData->mAlpha ? SurfaceFormat::OS_RGBA : SurfaceFormat::OS_RGBX; + Maybe animParams; + if (!IsFirstFrameDecode()) { + animParams.emplace(FullFrame().ToUnknownRect(), parsedImage.mDuration, + parsedImage.mFrameNum, BlendMethod::SOURCE, + DisposalMethod::CLEAR_ALL); + } + pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), FullFrame(), format, outFormat, animParams, + mTransform, SurfacePipeFlags()); + } else { + pipe = SurfacePipeFactory::CreateReorientSurfacePipe( + this, Size(), OutputSize(), format, mTransform, GetOrientation()); + } + + if (pipe.isNothing()) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] could not initialize surface pipe", this)); + return AsVariant(NonDecoderResult::PipeInitError); + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, ("[this=%p] writing to surface", this)); + const uint8_t* endOfRgbBuf = {rgbBuf.get() + rgbBufLength.value()}; + WriteState writeBufferResult = WriteState::NEED_MORE_DATA; + for (uint8_t* rowPtr = rgbBuf.get(); rowPtr < endOfRgbBuf; + rowPtr += rgbStride.value()) { + writeBufferResult = pipe->WriteBuffer(reinterpret_cast(rowPtr)); + + Maybe invalidRect = pipe->TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + if (writeBufferResult == WriteState::FAILURE) { + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] error writing rowPtr to surface pipe", this)); + + } else if (writeBufferResult == WriteState::FINISHED) { + MOZ_ASSERT(rowPtr + rgbStride.value() == endOfRgbBuf); + } + } + + MOZ_LOG(sAVIFLog, LogLevel::Debug, + ("[this=%p] writing to surface complete", this)); + + if (writeBufferResult == WriteState::FINISHED) { + PostFrameStop(mHasAlpha ? Opacity::SOME_TRANSPARENCY + : Opacity::FULLY_OPAQUE); + + if (!mIsAnimated || IsFirstFrameDecode()) { + PostDecodeDone(0); + return DecodeResult(NonDecoderResult::Complete); + } + + if (isDone) { + switch (mParser->GetInfo().loop_mode) { + case MP4PARSE_AVIF_LOOP_MODE_LOOP_BY_COUNT: { + auto loopCount = mParser->GetInfo().loop_count; + PostDecodeDone( + loopCount > INT32_MAX ? -1 : static_cast(loopCount)); + break; + } + case MP4PARSE_AVIF_LOOP_MODE_LOOP_INFINITELY: + case MP4PARSE_AVIF_LOOP_MODE_NO_EDITS: + default: + PostDecodeDone(-1); + break; + } + return DecodeResult(NonDecoderResult::Complete); + } + + return DecodeResult(NonDecoderResult::OutputAvailable); + } + + return AsVariant(NonDecoderResult::WriteBufferError); +} + +/* static */ +bool nsAVIFDecoder::IsDecodeSuccess(const DecodeResult& aResult) { + return aResult == DecodeResult(NonDecoderResult::OutputAvailable) || + aResult == DecodeResult(NonDecoderResult::Complete) || + aResult == DecodeResult(Dav1dResult(0)) || + aResult == DecodeResult(AOMResult(AOM_CODEC_OK)); +} + +void nsAVIFDecoder::RecordDecodeResultTelemetry( + const nsAVIFDecoder::DecodeResult& aResult) { + if (aResult.is()) { + switch (aResult.as()) { + case MP4PARSE_STATUS_OK: + MOZ_ASSERT_UNREACHABLE( + "Expect NonDecoderResult, Dav1dResult or AOMResult"); + return; + case MP4PARSE_STATUS_BAD_ARG: + case MP4PARSE_STATUS_INVALID: + case MP4PARSE_STATUS_UNSUPPORTED: + case MP4PARSE_STATUS_EOF: + case MP4PARSE_STATUS_IO: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::parse_error); + return; + case MP4PARSE_STATUS_OOM: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::out_of_memory); + return; + case MP4PARSE_STATUS_MISSING_AVIF_OR_AVIS_BRAND: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::missing_brand); + return; + case MP4PARSE_STATUS_FTYP_NOT_FIRST: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::ftyp_not_first); + return; + case MP4PARSE_STATUS_NO_IMAGE: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::no_image); + return; + case MP4PARSE_STATUS_MOOV_BAD_QUANTITY: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::multiple_moov); + return; + case MP4PARSE_STATUS_MOOV_MISSING: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::no_moov); + return; + case MP4PARSE_STATUS_LSEL_NO_ESSENTIAL: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::lsel_no_essential); + return; + case MP4PARSE_STATUS_A1OP_NO_ESSENTIAL: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::a1op_no_essential); + return; + case MP4PARSE_STATUS_A1LX_ESSENTIAL: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::a1lx_essential); + return; + case MP4PARSE_STATUS_TXFORM_NO_ESSENTIAL: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::txform_no_essential); + return; + case MP4PARSE_STATUS_PITM_MISSING: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::no_primary_item); + return; + case MP4PARSE_STATUS_IMAGE_ITEM_TYPE: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::image_item_type); + return; + case MP4PARSE_STATUS_ITEM_TYPE_MISSING: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::item_type_missing); + return; + case MP4PARSE_STATUS_CONSTRUCTION_METHOD: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::construction_method); + return; + case MP4PARSE_STATUS_PITM_NOT_FOUND: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::item_loc_not_found); + return; + case MP4PARSE_STATUS_IDAT_MISSING: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::no_item_data_box); + return; + default: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::uncategorized); + return; + } + + MOZ_LOG(sAVIFLog, LogLevel::Error, + ("[this=%p] unexpected Mp4parseStatus value: %d", this, + aResult.as())); + MOZ_ASSERT(false, "unexpected Mp4parseStatus value"); + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::invalid_parse_status); + + } else if (aResult.is()) { + switch (aResult.as()) { + case NonDecoderResult::NeedMoreData: + return; + case NonDecoderResult::OutputAvailable: + return; + case NonDecoderResult::Complete: + return; + case NonDecoderResult::SizeOverflow: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::size_overflow); + return; + case NonDecoderResult::OutOfMemory: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::out_of_memory); + return; + case NonDecoderResult::PipeInitError: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::pipe_init_error); + return; + case NonDecoderResult::WriteBufferError: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::write_buffer_error); + return; + case NonDecoderResult::AlphaYSizeMismatch: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::alpha_y_sz_mismatch); + return; + case NonDecoderResult::AlphaYColorDepthMismatch: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::alpha_y_bpc_mismatch); + return; + case NonDecoderResult::MetadataImageSizeMismatch: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::ispe_mismatch); + return; + case NonDecoderResult::RenderSizeMismatch: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::render_size_mismatch); + return; + case NonDecoderResult::FrameSizeChanged: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::frame_size_changed); + return; + case NonDecoderResult::InvalidCICP: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::invalid_cicp); + return; + case NonDecoderResult::NoSamples: + AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::no_samples); + return; + } + MOZ_ASSERT_UNREACHABLE("unknown NonDecoderResult"); + } else { + MOZ_ASSERT(aResult.is() || aResult.is()); + AccumulateCategorical(aResult.is() ? LABELS_AVIF_DECODER::dav1d + : LABELS_AVIF_DECODER::aom); + AccumulateCategorical(IsDecodeSuccess(aResult) + ? LABELS_AVIF_DECODE_RESULT::success + : LABELS_AVIF_DECODE_RESULT::decode_error); + } +} + +Maybe nsAVIFDecoder::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_AVIF); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsAVIFDecoder.h b/image/decoders/nsAVIFDecoder.h new file mode 100644 index 0000000000..59f6498202 --- /dev/null +++ b/image/decoders/nsAVIFDecoder.h @@ -0,0 +1,289 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsAVIFDecoder_h +#define mozilla_image_decoders_nsAVIFDecoder_h + +#include "Decoder.h" +#include "mozilla/gfx/Types.h" +#include "MP4Metadata.h" +#include "mp4parse.h" +#include "SampleIterator.h" +#include "SurfacePipe.h" + +#include "aom/aom_decoder.h" +#include "dav1d/dav1d.h" + +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace image { +class RasterImage; +class AVIFDecoderStream; +class AVIFParser; +class AVIFDecoderInterface; + +class nsAVIFDecoder final : public Decoder { + public: + virtual ~nsAVIFDecoder(); + + DecoderType GetType() const override { return DecoderType::AVIF; } + + protected: + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + Maybe SpeedHistogram() const override; + + private: + friend class DecoderFactory; + friend class AVIFDecoderInterface; + friend class AVIFParser; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsAVIFDecoder(RasterImage* aImage); + + static intptr_t ReadSource(uint8_t* aDestBuf, uintptr_t aDestBufSize, + void* aUserData); + + typedef int Dav1dResult; + enum class NonAOMCodecError { NoFrame, SizeOverflow }; + typedef Variant AOMResult; + enum class NonDecoderResult { + NeedMoreData, + OutputAvailable, + Complete, + SizeOverflow, + OutOfMemory, + PipeInitError, + WriteBufferError, + AlphaYSizeMismatch, + AlphaYColorDepthMismatch, + MetadataImageSizeMismatch, + RenderSizeMismatch, + FrameSizeChanged, + InvalidCICP, + NoSamples, + }; + using DecodeResult = + Variant; + Mp4parseStatus CreateParser(); + DecodeResult CreateDecoder(); + DecodeResult DoDecodeInternal(SourceBufferIterator& aIterator, + IResumable* aOnResume); + + static bool IsDecodeSuccess(const DecodeResult& aResult); + + void RecordDecodeResultTelemetry(const DecodeResult& aResult); + + Vector mBufferedData; + RefPtr mBufferStream; + + /// Pointer to the next place to read from mBufferedData + const uint8_t* mReadCursor = nullptr; + + UniquePtr mParser = nullptr; + UniquePtr mDecoder = nullptr; + + bool mIsAnimated = false; + bool mHasAlpha = false; +}; + +class AVIFDecoderStream : public ByteStream { + public: + explicit AVIFDecoderStream(Vector* aBuffer) { mBuffer = aBuffer; } + + virtual bool ReadAt(int64_t offset, void* data, size_t size, + size_t* bytes_read) override; + virtual bool CachedReadAt(int64_t offset, void* data, size_t size, + size_t* bytes_read) override { + return ReadAt(offset, data, size, bytes_read); + }; + virtual bool Length(int64_t* size) override; + virtual const uint8_t* GetContiguousAccess(int64_t aOffset, + size_t aSize) override; + + private: + Vector* mBuffer; +}; + +struct AVIFImage { + uint32_t mFrameNum = 0; + FrameTimeout mDuration = FrameTimeout::Zero(); + RefPtr mColorImage = nullptr; + RefPtr mAlphaImage = nullptr; +}; + +class AVIFParser { + public: + static Mp4parseStatus Create(const Mp4parseIo* aIo, ByteStream* aBuffer, + UniquePtr& aParserOut, + bool aAllowSequences, bool aAnimateAVIFMajor); + + ~AVIFParser(); + + const Mp4parseAvifInfo& GetInfo() const { return mInfo; } + + nsAVIFDecoder::DecodeResult GetImage(AVIFImage& aImage); + + bool IsAnimated() const; + + private: + explicit AVIFParser(const Mp4parseIo* aIo); + + Mp4parseStatus Init(ByteStream* aBuffer, bool aAllowSequences, + bool aAnimateAVIFMajor); + + struct FreeAvifParser { + void operator()(Mp4parseAvifParser* aPtr) { mp4parse_avif_free(aPtr); } + }; + + const Mp4parseIo* mIo; + UniquePtr mParser = nullptr; + Mp4parseAvifInfo mInfo = {}; + + UniquePtr mColorSampleIter = nullptr; + UniquePtr mAlphaSampleIter = nullptr; + uint32_t mFrameNum = 0; +}; + +struct Dav1dPictureUnref { + void operator()(Dav1dPicture* aPtr) { + dav1d_picture_unref(aPtr); + delete aPtr; + } +}; + +using OwnedDav1dPicture = UniquePtr; + +class OwnedAOMImage { + public: + ~OwnedAOMImage(); + + static OwnedAOMImage* CopyFrom(aom_image_t* aImage, bool aIsAlpha); + + aom_image_t* GetImage() { return mImage.isSome() ? mImage.ptr() : nullptr; } + + private: + OwnedAOMImage(); + + bool CloneFrom(aom_image_t* aImage, bool aIsAlpha); + + // The mImage's planes are referenced to mBuffer + Maybe mImage; + UniquePtr mBuffer; +}; + +struct AVIFDecodedData : layers::PlanarYCbCrData { + public: + Maybe mRenderSize = Nothing(); + gfx::CICP::ColourPrimaries mColourPrimaries = gfx::CICP::CP_UNSPECIFIED; + gfx::CICP::TransferCharacteristics mTransferCharacteristics = + gfx::CICP::TC_UNSPECIFIED; + gfx::CICP::MatrixCoefficients mMatrixCoefficients = gfx::CICP::MC_UNSPECIFIED; + + OwnedDav1dPicture mColorDav1d; + OwnedDav1dPicture mAlphaDav1d; + UniquePtr mColorAOM; + UniquePtr mAlphaAOM; + + // CICP values (either from the BMFF container or the AV1 sequence header) are + // used to create the colorspace transform. CICP::MatrixCoefficients is only + // stored for the sake of telemetry, since the relevant information for YUV -> + // RGB conversion is stored in mYUVColorSpace. + // + // There are three potential sources of color information for an AVIF: + // 1. ICC profile via a ColourInformationBox (colr) defined in [ISOBMFF] + // § 12.1.5 "Colour information" and [MIAF] § 7.3.6.4 "Colour information + // property" + // 2. NCLX (AKA CICP see [ITU-T H.273]) values in the same + // ColourInformationBox + // which can have an ICC profile or NCLX values, not both). + // 3. NCLX values in the AV1 bitstream + // + // The 'colr' box is optional, but there are always CICP values in the AV1 + // bitstream, so it is possible to have both. Per ISOBMFF § 12.1.5.1 + // > If colour information is supplied in both this box, and also in the + // > video bitstream, this box takes precedence, and over-rides the + // > information in the bitstream. + // + // If present, the ICC profile takes precedence over CICP values, but only + // specifies the color space, not the matrix coefficients necessary to convert + // YCbCr data (as most AVIF are encoded) to RGB. The matrix coefficients are + // always derived from the CICP values for matrix_coefficients (and + // potentially colour_primaries, but in that case only the CICP values for + // colour_primaries will be used, not anything harvested from the ICC + // profile). + // + // If there is no ICC profile, the color space transform will be based on the + // CICP values either from the 'colr' box, or if absent/unspecified, the + // decoded AV1 sequence header. + // + // For values that are 2 (meaning unspecified) after trying both, the + // fallback values are: + // - CP: 1 (BT.709/sRGB) + // - TC: 13 (sRGB) + // - MC: 6 (BT.601) + // - Range: Full + // + // Additional details here: + // . Note + // that this contradicts the current version of [MIAF] § 7.3.6.4 which + // specifies MC=1 (BT.709). This is revised in [MIAF DAMD2] and confirmed by + // + // + // The precedence for applying the various values and defaults in the event + // no valid values are found are managed by the following functions. + // + // References: + // [ISOBMFF]: ISO/IEC 14496-12:2020 + // [MIAF]: ISO/IEC 23000-22:2019 + // [MIAF DAMD2]: ISO/IEC 23000-22:2019/FDAmd 2 + // + // [ITU-T H.273]: Rec. ITU-T H.273 (12/2016) + // + void SetCicpValues( + const Mp4parseNclxColourInformation* aNclx, + const gfx::CICP::ColourPrimaries aAv1ColourPrimaries, + const gfx::CICP::TransferCharacteristics aAv1TransferCharacteristics, + const gfx::CICP::MatrixCoefficients aAv1MatrixCoefficients); +}; + +// An interface to do decode and get the decoded data +class AVIFDecoderInterface { + public: + using Dav1dResult = nsAVIFDecoder::Dav1dResult; + using NonAOMCodecError = nsAVIFDecoder::NonAOMCodecError; + using AOMResult = nsAVIFDecoder::AOMResult; + using NonDecoderResult = nsAVIFDecoder::NonDecoderResult; + using DecodeResult = nsAVIFDecoder::DecodeResult; + + virtual ~AVIFDecoderInterface() = default; + + // Set the mDecodedData if Decode() succeeds + virtual DecodeResult Decode(bool aShouldSendTelemetry, + const Mp4parseAvifInfo& aAVIFInfo, + const AVIFImage& aSamples) = 0; + // Must be called only once after Decode() succeeds + UniquePtr GetDecodedData() { + MOZ_ASSERT(mDecodedData); + return std::move(mDecodedData); + } + + protected: + explicit AVIFDecoderInterface() = default; + + inline static bool IsDecodeSuccess(const DecodeResult& aResult) { + return nsAVIFDecoder::IsDecodeSuccess(aResult); + } + + // The mDecodedData is valid after Decode() succeeds + UniquePtr mDecodedData; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsAVIFDecoder_h diff --git a/image/decoders/nsBMPDecoder.cpp b/image/decoders/nsBMPDecoder.cpp new file mode 100644 index 0000000000..da971e054f --- /dev/null +++ b/image/decoders/nsBMPDecoder.cpp @@ -0,0 +1,1275 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This is a cross-platform BMP Decoder, which should work everywhere, +// including big-endian machines like the PowerPC. +// +// BMP is a format that has been extended multiple times. To understand the +// decoder you need to understand this history. The summary of the history +// below was determined from the following documents. +// +// - http://www.fileformat.info/format/bmp/egff.htm +// - http://www.fileformat.info/format/os2bmp/egff.htm +// - http://fileformats.archiveteam.org/wiki/BMP +// - http://fileformats.archiveteam.org/wiki/OS/2_BMP +// - https://en.wikipedia.org/wiki/BMP_file_format +// - https://upload.wikimedia.org/wikipedia/commons/c/c4/BMPfileFormat.png +// +// WINDOWS VERSIONS OF THE BMP FORMAT +// ---------------------------------- +// WinBMPv1. +// - This version is no longer used and can be ignored. +// +// WinBMPv2. +// - First is a 14 byte file header that includes: the magic number ("BM"), +// file size, and offset to the pixel data (|mDataOffset|). +// - Next is a 12 byte info header which includes: the info header size +// (mBIHSize), width, height, number of color planes, and bits-per-pixel +// (|mBpp|) which must be 1, 4, 8 or 24. +// - Next is the semi-optional color table, which has length 2^|mBpp| and has 3 +// bytes per value (BGR). The color table is required if |mBpp| is 1, 4, or 8. +// - Next is an optional gap. +// - Next is the pixel data, which is pointed to by |mDataOffset|. +// +// WinBMPv3. This is the most widely used version. +// - It changed the info header to 40 bytes by taking the WinBMPv2 info +// header, enlargening its width and height fields, and adding more fields +// including: a compression type (|mCompression|) and number of colors +// (|mNumColors|). +// - The semi-optional color table is now 4 bytes per value (BGR0), and its +// length is |mNumColors|, or 2^|mBpp| if |mNumColors| is zero. +// - |mCompression| can be RGB (i.e. no compression), RLE4 (if |mBpp|==4) or +// RLE8 (if |mBpp|==8) values. +// +// WinBMPv3-NT. A variant of WinBMPv3. +// - It did not change the info header layout from WinBMPv3. +// - |mBpp| can now be 16 or 32, in which case |mCompression| can be RGB or the +// new BITFIELDS value; in the latter case an additional 12 bytes of color +// bitfields follow the info header. +// +// WinBMPv4. +// - It extended the info header to 108 bytes, including the 12 bytes of color +// mask data from WinBMPv3-NT, plus alpha mask data, and also color-space and +// gamma correction fields. +// +// WinBMPv5. +// - It extended the info header to 124 bytes, adding color profile data. +// - It also added an optional color profile table after the pixel data (and +// another optional gap). +// +// WinBMPv3-ICO. This is a variant of WinBMPv3. +// - It's the BMP format used for BMP images within ICO files. +// - The only difference with WinBMPv3 is that if an image is 32bpp and has no +// compression, then instead of treating the pixel data as 0RGB it is treated +// as ARGB, but only if one or more of the A values are non-zero. +// +// Clipboard variants. +// - It's the BMP format used for BMP images captured from the clipboard. +// - It is missing the file header, containing the BM signature and the data +// offset. Instead the data begins after the header. +// - If it uses BITFIELDS compression, then there is always an additional 12 +// bytes of data after the header that must be read. In WinBMPv4+, the masks +// are supposed to be included in the header size, which are the values we use +// for decoding purposes, but there is additional three masks following the +// header which must be skipped to get to the pixel data. +// +// OS/2 VERSIONS OF THE BMP FORMAT +// ------------------------------- +// OS2-BMPv1. +// - Almost identical to WinBMPv2; the differences are basically ignorable. +// +// OS2-BMPv2. +// - Similar to WinBMPv3. +// - The info header is 64 bytes but can be reduced to as little as 16; any +// omitted fields are treated as zero. The first 40 bytes of these fields are +// nearly identical to the WinBMPv3 info header; the remaining 24 bytes are +// different. +// - Also adds compression types "Huffman 1D" and "RLE24", which we don't +// support. +// - We treat OS2-BMPv2 files as if they are WinBMPv3 (i.e. ignore the extra 24 +// bytes in the info header), which in practice is good enough. + +#include "ImageLogging.h" +#include "nsBMPDecoder.h" + +#include + +#include "mozilla/Attributes.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Likely.h" +#include "mozilla/UniquePtrExtensions.h" + +#include "RasterImage.h" +#include "SurfacePipeFactory.h" +#include "gfxPlatform.h" +#include + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { +namespace bmp { + +struct Compression { + enum { RGB = 0, RLE8 = 1, RLE4 = 2, BITFIELDS = 3 }; +}; + +// RLE escape codes and constants. +struct RLE { + enum { + ESCAPE = 0, + ESCAPE_EOL = 0, + ESCAPE_EOF = 1, + ESCAPE_DELTA = 2, + + SEGMENT_LENGTH = 2, + DELTA_LENGTH = 2 + }; +}; + +} // namespace bmp + +using namespace bmp; + +static double FixedPoint2Dot30_To_Double(uint32_t aFixed) { + constexpr double factor = 1.0 / 1073741824.0; // 2^-30 + return double(aFixed) * factor; +} + +static float FixedPoint16Dot16_To_Float(uint32_t aFixed) { + constexpr double factor = 1.0 / 65536.0; // 2^-16 + return double(aFixed) * factor; +} + +static float CalRbgEndpointToQcms(const CalRgbEndpoint& aIn, + qcms_CIE_xyY& aOut) { + aOut.x = FixedPoint2Dot30_To_Double(aIn.mX); + aOut.y = FixedPoint2Dot30_To_Double(aIn.mY); + aOut.Y = FixedPoint2Dot30_To_Double(aIn.mZ); + return FixedPoint16Dot16_To_Float(aIn.mGamma); +} + +static void ReadCalRgbEndpoint(const char* aData, uint32_t aEndpointOffset, + uint32_t aGammaOffset, CalRgbEndpoint& aOut) { + aOut.mX = LittleEndian::readUint32(aData + aEndpointOffset); + aOut.mY = LittleEndian::readUint32(aData + aEndpointOffset + 4); + aOut.mZ = LittleEndian::readUint32(aData + aEndpointOffset + 8); + aOut.mGamma = LittleEndian::readUint32(aData + aGammaOffset); +} + +/// Sets the pixel data in aDecoded to the given values. +/// @param aDecoded pointer to pixel to be set, will be incremented to point to +/// the next pixel. +static void SetPixel(uint32_t*& aDecoded, uint8_t aRed, uint8_t aGreen, + uint8_t aBlue, uint8_t aAlpha = 0xFF) { + *aDecoded++ = gfxPackedPixelNoPreMultiply(aAlpha, aRed, aGreen, aBlue); +} + +static void SetPixel(uint32_t*& aDecoded, uint8_t idx, + const UniquePtr& aColors) { + SetPixel(aDecoded, aColors[idx].mRed, aColors[idx].mGreen, + aColors[idx].mBlue); +} + +/// Sets two (or one if aCount = 1) pixels +/// @param aDecoded where the data is stored. Will be moved 4 resp 8 bytes +/// depending on whether one or two pixels are written. +/// @param aData The values for the two pixels +/// @param aCount Current count. Is decremented by one or two. +static void Set4BitPixel(uint32_t*& aDecoded, uint8_t aData, uint32_t& aCount, + const UniquePtr& aColors) { + uint8_t idx = aData >> 4; + SetPixel(aDecoded, idx, aColors); + if (--aCount > 0) { + idx = aData & 0xF; + SetPixel(aDecoded, idx, aColors); + --aCount; + } +} + +static mozilla::LazyLogModule sBMPLog("BMPDecoder"); + +// The length of the mBIHSize field in the info header. +static const uint32_t BIHSIZE_FIELD_LENGTH = 4; + +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage, State aState, size_t aLength, + bool aForClipboard) + : Decoder(aImage), + mLexer(Transition::To(aState, aLength), Transition::TerminateSuccess()), + mIsWithinICO(false), + mIsForClipboard(aForClipboard), + mMayHaveTransparency(false), + mDoesHaveTransparency(false), + mNumColors(0), + mColors(nullptr), + mBytesPerColor(0), + mPreGapLength(0), + mPixelRowSize(0), + mCurrentRow(0), + mCurrentPos(0), + mAbsoluteModeNumPixels(0) {} + +// Constructor for normal BMP files or from the clipboard. +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage, bool aForClipboard) + : nsBMPDecoder(aImage, + aForClipboard ? State::INFO_HEADER_SIZE : State::FILE_HEADER, + aForClipboard ? BIHSIZE_FIELD_LENGTH : FILE_HEADER_LENGTH, + aForClipboard) {} + +// Constructor used for WinBMPv3-ICO files, which lack a file header. +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage, uint32_t aDataOffset) + : nsBMPDecoder(aImage, State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH, + /* aForClipboard */ false) { + SetIsWithinICO(); + + // Even though the file header isn't present in this case, the dataOffset + // field is set as if it is, and so we must increment mPreGapLength + // accordingly. + mPreGapLength += FILE_HEADER_LENGTH; + + // This is the one piece of data we normally get from a BMP file header, so + // it must be provided via an argument. + mH.mDataOffset = aDataOffset; +} + +nsBMPDecoder::~nsBMPDecoder() {} + +// Obtains the size of the compressed image resource. +int32_t nsBMPDecoder::GetCompressedImageSize() const { + // In the RGB case mImageSize might not be set, so compute it manually. + MOZ_ASSERT(mPixelRowSize != 0); + return mH.mCompression == Compression::RGB ? mPixelRowSize * AbsoluteHeight() + : mH.mImageSize; +} + +nsresult nsBMPDecoder::BeforeFinishInternal() { + if (!IsMetadataDecode() && !mImageData) { + return NS_ERROR_FAILURE; // No image; something went wrong. + } + + return NS_OK; +} + +nsresult nsBMPDecoder::FinishInternal() { + // We shouldn't be called in error cases. + MOZ_ASSERT(!HasError(), "Can't call FinishInternal on error!"); + + // We should never make multiple frames. + MOZ_ASSERT(GetFrameCount() <= 1, "Multiple BMP frames?"); + + // Send notifications if appropriate. + if (!IsMetadataDecode() && HasSize()) { + // We should have image data. + MOZ_ASSERT(mImageData); + + // If it was truncated, fill in the missing pixels as black. + while (mCurrentRow > 0) { + uint32_t* dst = RowBuffer(); + while (mCurrentPos < mH.mWidth) { + SetPixel(dst, 0, 0, 0); + mCurrentPos++; + } + mCurrentPos = 0; + FinishRow(); + } + + MOZ_ASSERT_IF(mDoesHaveTransparency, mMayHaveTransparency); + + // We have transparency if we either detected some in the image itself + // (i.e., |mDoesHaveTransparency| is true) or we're in an ICO, which could + // mean we have an AND mask that provides transparency (i.e., |mIsWithinICO| + // is true). + // XXX(seth): We can tell when we create the decoder if the AND mask is + // present, so we could be more precise about this. + const Opacity opacity = mDoesHaveTransparency || mIsWithinICO + ? Opacity::SOME_TRANSPARENCY + : Opacity::FULLY_OPAQUE; + + PostFrameStop(opacity); + PostDecodeDone(); + } + + return NS_OK; +} + +// ---------------------------------------- +// Actual Data Processing +// ---------------------------------------- + +void BitFields::Value::Set(uint32_t aMask) { + mMask = aMask; + + // Handle this exceptional case first. The chosen values don't matter + // (because a mask of zero will always give a value of zero) except that + // mBitWidth: + // - shouldn't be zero, because that would cause an infinite loop in Get(); + // - shouldn't be 5 or 8, because that could cause a false positive match in + // IsR5G5B5() or IsR8G8B8(). + if (mMask == 0x0) { + mRightShift = 0; + mBitWidth = 1; + return; + } + + // Find the rightmost 1. + uint8_t i; + for (i = 0; i < 32; i++) { + if (mMask & (1 << i)) { + break; + } + } + mRightShift = i; + + // Now find the leftmost 1 in the same run of 1s. (If there are multiple runs + // of 1s -- which isn't valid -- we'll behave as if only the lowest run was + // present, which seems reasonable.) + for (i = i + 1; i < 32; i++) { + if (!(mMask & (1 << i))) { + break; + } + } + mBitWidth = i - mRightShift; +} + +MOZ_ALWAYS_INLINE uint8_t BitFields::Value::Get(uint32_t aValue) const { + // Extract the unscaled value. + uint32_t v = (aValue & mMask) >> mRightShift; + + // Idea: to upscale v precisely we need to duplicate its bits, possibly + // repeatedly, possibly partially in the last case, from bit 7 down to bit 0 + // in v2. For example: + // + // - mBitWidth=1: v2 = v<<7 | v<<6 | ... | v<<1 | v>>0 k -> kkkkkkkk + // - mBitWidth=2: v2 = v<<6 | v<<4 | v<<2 | v>>0 jk -> jkjkjkjk + // - mBitWidth=3: v2 = v<<5 | v<<2 | v>>1 ijk -> ijkijkij + // - mBitWidth=4: v2 = v<<4 | v>>0 hijk -> hijkhijk + // - mBitWidth=5: v2 = v<<3 | v>>2 ghijk -> ghijkghi + // - mBitWidth=6: v2 = v<<2 | v>>4 fghijk -> fghijkfg + // - mBitWidth=7: v2 = v<<1 | v>>6 efghijk -> efghijke + // - mBitWidth=8: v2 = v>>0 defghijk -> defghijk + // - mBitWidth=9: v2 = v>>1 cdefghijk -> cdefghij + // - mBitWidth=10: v2 = v>>2 bcdefghijk -> bcdefghi + // - mBitWidth=11: v2 = v>>3 abcdefghijk -> abcdefgh + // - etc. + // + uint8_t v2 = 0; + int32_t i; // must be a signed integer + for (i = 8 - mBitWidth; i > 0; i -= mBitWidth) { + v2 |= v << uint32_t(i); + } + v2 |= v >> uint32_t(-i); + return v2; +} + +MOZ_ALWAYS_INLINE uint8_t BitFields::Value::GetAlpha(uint32_t aValue, + bool& aHasAlphaOut) const { + if (mMask == 0x0) { + return 0xff; + } + aHasAlphaOut = true; + return Get(aValue); +} + +MOZ_ALWAYS_INLINE uint8_t BitFields::Value::Get5(uint32_t aValue) const { + MOZ_ASSERT(mBitWidth == 5); + uint32_t v = (aValue & mMask) >> mRightShift; + return (v << 3u) | (v >> 2u); +} + +MOZ_ALWAYS_INLINE uint8_t BitFields::Value::Get8(uint32_t aValue) const { + MOZ_ASSERT(mBitWidth == 8); + uint32_t v = (aValue & mMask) >> mRightShift; + return v; +} + +void BitFields::SetR5G5B5() { + mRed.Set(0x7c00); + mGreen.Set(0x03e0); + mBlue.Set(0x001f); +} + +void BitFields::SetR8G8B8() { + mRed.Set(0xff0000); + mGreen.Set(0xff00); + mBlue.Set(0x00ff); +} + +bool BitFields::IsR5G5B5() const { + return mRed.mBitWidth == 5 && mGreen.mBitWidth == 5 && mBlue.mBitWidth == 5 && + mAlpha.mMask == 0x0; +} + +bool BitFields::IsR8G8B8() const { + return mRed.mBitWidth == 8 && mGreen.mBitWidth == 8 && mBlue.mBitWidth == 8 && + mAlpha.mMask == 0x0; +} + +uint32_t* nsBMPDecoder::RowBuffer() { return mRowBuffer.get() + mCurrentPos; } + +void nsBMPDecoder::ClearRowBufferRemainder() { + int32_t len = mH.mWidth - mCurrentPos; + memset(RowBuffer(), mMayHaveTransparency ? 0 : 0xFF, len * sizeof(uint32_t)); +} + +void nsBMPDecoder::FinishRow() { + mPipe.WriteBuffer(mRowBuffer.get()); + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + mCurrentRow--; +} + +LexerResult nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex( + aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::FILE_HEADER: + return ReadFileHeader(aData, aLength); + case State::INFO_HEADER_SIZE: + return ReadInfoHeaderSize(aData, aLength); + case State::INFO_HEADER_REST: + return ReadInfoHeaderRest(aData, aLength); + case State::BITFIELDS: + return ReadBitfields(aData, aLength); + case State::SKIP_TO_COLOR_PROFILE: + return Transition::ContinueUnbuffered(State::SKIP_TO_COLOR_PROFILE); + case State::FOUND_COLOR_PROFILE: + return Transition::To(State::COLOR_PROFILE, + mH.mColorSpace.mProfile.mLength); + case State::COLOR_PROFILE: + return ReadColorProfile(aData, aLength); + case State::ALLOCATE_SURFACE: + return AllocateSurface(); + case State::COLOR_TABLE: + return ReadColorTable(aData, aLength); + case State::GAP: + return SkipGap(); + case State::AFTER_GAP: + return AfterGap(); + case State::PIXEL_ROW: + return ReadPixelRow(aData); + case State::RLE_SEGMENT: + return ReadRLESegment(aData); + case State::RLE_DELTA: + return ReadRLEDelta(aData); + case State::RLE_ABSOLUTE: + return ReadRLEAbsolute(aData, aLength); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition nsBMPDecoder::ReadFileHeader( + const char* aData, size_t aLength) { + mPreGapLength += aLength; + + bool signatureOk = aData[0] == 'B' && aData[1] == 'M'; + if (!signatureOk) { + return Transition::TerminateFailure(); + } + + // We ignore the filesize (aData + 2) and reserved (aData + 6) fields. + + mH.mDataOffset = LittleEndian::readUint32(aData + 10); + + return Transition::To(State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH); +} + +// We read the info header in two steps: (a) read the mBIHSize field to +// determine how long the header is; (b) read the rest of the header. +LexerTransition nsBMPDecoder::ReadInfoHeaderSize( + const char* aData, size_t aLength) { + mH.mBIHSize = LittleEndian::readUint32(aData); + + // Data offset can be wrong so fix it using the BIH size. + if (!mIsForClipboard && mH.mDataOffset < mPreGapLength + mH.mBIHSize) { + mH.mDataOffset = mPreGapLength + mH.mBIHSize; + } + + mPreGapLength += aLength; + + bool bihSizeOk = mH.mBIHSize == InfoHeaderLength::WIN_V2 || + mH.mBIHSize == InfoHeaderLength::WIN_V3 || + mH.mBIHSize == InfoHeaderLength::WIN_V4 || + mH.mBIHSize == InfoHeaderLength::WIN_V5 || + (mH.mBIHSize >= InfoHeaderLength::OS2_V2_MIN && + mH.mBIHSize <= InfoHeaderLength::OS2_V2_MAX); + if (!bihSizeOk) { + return Transition::TerminateFailure(); + } + // ICO BMPs must have a WinBMPv3 header. nsICODecoder should have already + // terminated decoding if this isn't the case. + MOZ_ASSERT_IF(mIsWithinICO, mH.mBIHSize == InfoHeaderLength::WIN_V3); + + return Transition::To(State::INFO_HEADER_REST, + mH.mBIHSize - BIHSIZE_FIELD_LENGTH); +} + +LexerTransition nsBMPDecoder::ReadInfoHeaderRest( + const char* aData, size_t aLength) { + mPreGapLength += aLength; + + // |mWidth| and |mHeight| may be signed (Windows) or unsigned (OS/2). We just + // read as unsigned because in practice that's good enough. + if (mH.mBIHSize == InfoHeaderLength::WIN_V2) { + mH.mWidth = LittleEndian::readUint16(aData + 0); + mH.mHeight = LittleEndian::readUint16(aData + 2); + // We ignore the planes (aData + 4) field; it should always be 1. + mH.mBpp = LittleEndian::readUint16(aData + 6); + } else { + mH.mWidth = LittleEndian::readUint32(aData + 0); + mH.mHeight = LittleEndian::readUint32(aData + 4); + // We ignore the planes (aData + 4) field; it should always be 1. + mH.mBpp = LittleEndian::readUint16(aData + 10); + + // For OS2-BMPv2 the info header may be as little as 16 bytes, so be + // careful for these fields. + mH.mCompression = aLength >= 16 ? LittleEndian::readUint32(aData + 12) : 0; + mH.mImageSize = aLength >= 20 ? LittleEndian::readUint32(aData + 16) : 0; + // We ignore the xppm (aData + 20) and yppm (aData + 24) fields. + mH.mNumColors = aLength >= 32 ? LittleEndian::readUint32(aData + 28) : 0; + // We ignore the important_colors (aData + 36) field. + + // Read color management properties we may need later. + mH.mCsType = + aLength >= 56 + ? static_cast(LittleEndian::readUint32(aData + 52)) + : InfoColorSpace::SRGB; + mH.mCsIntent = aLength >= 108 ? static_cast( + LittleEndian::readUint32(aData + 104)) + : InfoColorIntent::IMAGES; + + switch (mH.mCsType) { + case InfoColorSpace::CALIBRATED_RGB: + if (aLength >= 104) { + ReadCalRgbEndpoint(aData, 56, 92, mH.mColorSpace.mCalibrated.mRed); + ReadCalRgbEndpoint(aData, 68, 96, mH.mColorSpace.mCalibrated.mGreen); + ReadCalRgbEndpoint(aData, 80, 100, mH.mColorSpace.mCalibrated.mBlue); + } else { + mH.mCsType = InfoColorSpace::SRGB; + } + break; + case InfoColorSpace::EMBEDDED: + if (aLength >= 116) { + mH.mColorSpace.mProfile.mOffset = + LittleEndian::readUint32(aData + 108); + mH.mColorSpace.mProfile.mLength = + LittleEndian::readUint32(aData + 112); + } else { + mH.mCsType = InfoColorSpace::SRGB; + } + break; + case InfoColorSpace::LINKED: + case InfoColorSpace::SRGB: + case InfoColorSpace::WIN: + default: + // Nothing to be done at this time. + break; + } + + // For WinBMPv4, WinBMPv5 and (possibly) OS2-BMPv2 there are additional + // fields in the info header which we ignore, with the possible exception + // of the color bitfields (see below). + } + + // The height for BMPs embedded inside an ICO includes spaces for the AND + // mask even if it is not present, thus we need to adjust for that here. + if (mIsWithinICO) { + // XXX(seth): Should we really be writing the absolute value from + // the BIH below? Seems like this could be problematic for inverted BMPs. + mH.mHeight = abs(mH.mHeight) / 2; + } + + // Run with MOZ_LOG=BMPDecoder:5 set to see this output. + MOZ_LOG(sBMPLog, LogLevel::Debug, + ("BMP: bihsize=%u, %d x %d, bpp=%u, compression=%u, colors=%u, " + "data-offset=%u\n", + mH.mBIHSize, mH.mWidth, mH.mHeight, uint32_t(mH.mBpp), + mH.mCompression, mH.mNumColors, mH.mDataOffset)); + + // BMPs with negative width are invalid. Also, reject extremely wide images + // to keep the math sane. And reject INT_MIN as a height because you can't + // get its absolute value (because -INT_MIN is one more than INT_MAX). + const int32_t k64KWidth = 0x0000FFFF; + bool sizeOk = + 0 <= mH.mWidth && mH.mWidth <= k64KWidth && mH.mHeight != INT_MIN; + if (!sizeOk) { + return Transition::TerminateFailure(); + } + + // Check mBpp and mCompression. + bool bppCompressionOk = + (mH.mCompression == Compression::RGB && + (mH.mBpp == 1 || mH.mBpp == 4 || mH.mBpp == 8 || mH.mBpp == 16 || + mH.mBpp == 24 || mH.mBpp == 32)) || + (mH.mCompression == Compression::RLE8 && mH.mBpp == 8) || + (mH.mCompression == Compression::RLE4 && mH.mBpp == 4) || + (mH.mCompression == Compression::BITFIELDS && + // For BITFIELDS compression we require an exact match for one of the + // WinBMP BIH sizes; this clearly isn't an OS2 BMP. + (mH.mBIHSize == InfoHeaderLength::WIN_V3 || + mH.mBIHSize == InfoHeaderLength::WIN_V4 || + mH.mBIHSize == InfoHeaderLength::WIN_V5) && + (mH.mBpp == 16 || mH.mBpp == 32)); + if (!bppCompressionOk) { + return Transition::TerminateFailure(); + } + + // Initialize our current row to the top of the image. + mCurrentRow = AbsoluteHeight(); + + // Round it up to the nearest byte count, then pad to 4-byte boundary. + // Compute this even for a metadate decode because GetCompressedImageSize() + // relies on it. + mPixelRowSize = (mH.mBpp * mH.mWidth + 7) / 8; + uint32_t surplus = mPixelRowSize % 4; + if (surplus != 0) { + mPixelRowSize += 4 - surplus; + } + + size_t bitFieldsLengthStillToRead = 0; + if (mH.mCompression == Compression::BITFIELDS) { + // Need to read bitfields. + if (mH.mBIHSize >= InfoHeaderLength::WIN_V4) { + // Bitfields are present in the info header, so we can read them + // immediately. + mBitFields.ReadFromHeader(aData + 36, /* aReadAlpha = */ true); + + // If this came from the clipboard, then we know that even if the header + // explicitly includes the bitfield masks, we need to add an additional + // offset for the start of the RGB data. + if (mIsForClipboard) { + mH.mDataOffset += BitFields::LENGTH; + } + } else { + // Bitfields are present after the info header, so we will read them in + // ReadBitfields(). + bitFieldsLengthStillToRead = BitFields::LENGTH; + } + } else if (mH.mBpp == 16) { + // No bitfields specified; use the default 5-5-5 values. + mBitFields.SetR5G5B5(); + } else if (mH.mBpp == 32) { + // No bitfields specified; use the default 8-8-8 values. + mBitFields.SetR8G8B8(); + } + + return Transition::To(State::BITFIELDS, bitFieldsLengthStillToRead); +} + +void BitFields::ReadFromHeader(const char* aData, bool aReadAlpha) { + mRed.Set(LittleEndian::readUint32(aData + 0)); + mGreen.Set(LittleEndian::readUint32(aData + 4)); + mBlue.Set(LittleEndian::readUint32(aData + 8)); + if (aReadAlpha) { + mAlpha.Set(LittleEndian::readUint32(aData + 12)); + } +} + +LexerTransition nsBMPDecoder::ReadBitfields( + const char* aData, size_t aLength) { + mPreGapLength += aLength; + + // If aLength is zero there are no bitfields to read, or we already read them + // in ReadInfoHeader(). + if (aLength != 0) { + mBitFields.ReadFromHeader(aData, /* aReadAlpha = */ false); + } + + // Note that RLE-encoded BMPs might be transparent because the 'delta' mode + // can skip pixels and cause implicit transparency. + mMayHaveTransparency = mIsWithinICO || mH.mCompression == Compression::RLE8 || + mH.mCompression == Compression::RLE4 || + (mH.mCompression == Compression::BITFIELDS && + mBitFields.mAlpha.IsPresent()); + if (mMayHaveTransparency) { + PostHasTransparency(); + } + + // Post our size to the superclass. + PostSize(mH.mWidth, AbsoluteHeight()); + if (HasError()) { + return Transition::TerminateFailure(); + } + + // We've now read all the headers. If we're doing a metadata decode, we're + // done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // Set up the color table, if present; it'll be filled in by ReadColorTable(). + if (mH.mBpp <= 8) { + mNumColors = 1 << mH.mBpp; + if (0 < mH.mNumColors && mH.mNumColors < mNumColors) { + mNumColors = mH.mNumColors; + } + + // Always allocate and zero 256 entries, even though mNumColors might be + // smaller, because the file might erroneously index past mNumColors. + mColors = MakeUniqueFallible(256); + if (NS_WARN_IF(!mColors)) { + return Transition::TerminateFailure(); + } + memset(mColors.get(), 0, 256 * sizeof(ColorTableEntry)); + + // OS/2 Bitmaps have no padding byte. + mBytesPerColor = (mH.mBIHSize == InfoHeaderLength::WIN_V2) ? 3 : 4; + } + + if (mCMSMode != CMSMode::Off) { + switch (mH.mCsType) { + case InfoColorSpace::EMBEDDED: + return SeekColorProfile(aLength); + case InfoColorSpace::CALIBRATED_RGB: + PrepareCalibratedColorProfile(); + break; + case InfoColorSpace::SRGB: + case InfoColorSpace::WIN: + MOZ_LOG(sBMPLog, LogLevel::Debug, ("using sRGB color profile\n")); + if (mColors) { + // We will transform the color table instead of the output pixels. + mTransform = GetCMSsRGBTransform(SurfaceFormat::R8G8B8); + } else { + mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBA); + } + break; + case InfoColorSpace::LINKED: + default: + // Not supported, no color management. + MOZ_LOG(sBMPLog, LogLevel::Debug, ("color space type not provided\n")); + break; + } + } + + return Transition::To(State::ALLOCATE_SURFACE, 0); +} + +void nsBMPDecoder::PrepareCalibratedColorProfile() { + // BMP does not define a white point. Use the same as sRGB. This matches what + // Chrome does as well. + qcms_CIE_xyY white_point = qcms_white_point_sRGB(); + + qcms_CIE_xyYTRIPLE primaries; + float redGamma = + CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mRed, primaries.red); + float greenGamma = + CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mGreen, primaries.green); + float blueGamma = + CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mBlue, primaries.blue); + + // Explicitly verify the profile because sometimes the values from the BMP + // header are just garbage. + mInProfile = qcms_profile_create_rgb_with_gamma_set( + white_point, primaries, redGamma, greenGamma, blueGamma); + if (mInProfile && qcms_profile_is_bogus(mInProfile)) { + // Bad profile, just use sRGB instead. Release the profile here, so that + // our destructor doesn't assume we are the owner for the transform. + qcms_profile_release(mInProfile); + mInProfile = nullptr; + } + + if (mInProfile) { + MOZ_LOG(sBMPLog, LogLevel::Debug, ("using calibrated RGB color profile\n")); + PrepareColorProfileTransform(); + } else { + MOZ_LOG(sBMPLog, LogLevel::Debug, + ("failed to create calibrated RGB color profile, using sRGB\n")); + if (mColors) { + // We will transform the color table instead of the output pixels. + mTransform = GetCMSsRGBTransform(SurfaceFormat::R8G8B8); + } else { + mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBA); + } + } +} + +void nsBMPDecoder::PrepareColorProfileTransform() { + if (!mInProfile || !GetCMSOutputProfile()) { + return; + } + + qcms_data_type inType; + qcms_data_type outType; + if (mColors) { + // We will transform the color table instead of the output pixels. + inType = QCMS_DATA_RGB_8; + outType = QCMS_DATA_RGB_8; + } else { + inType = gfxPlatform::GetCMSOSRGBAType(); + outType = inType; + } + + qcms_intent intent; + switch (mH.mCsIntent) { + case InfoColorIntent::BUSINESS: + intent = QCMS_INTENT_SATURATION; + break; + case InfoColorIntent::GRAPHICS: + intent = QCMS_INTENT_RELATIVE_COLORIMETRIC; + break; + case InfoColorIntent::ABS_COLORIMETRIC: + intent = QCMS_INTENT_ABSOLUTE_COLORIMETRIC; + break; + case InfoColorIntent::IMAGES: + default: + intent = QCMS_INTENT_PERCEPTUAL; + break; + } + + mTransform = qcms_transform_create(mInProfile, inType, GetCMSOutputProfile(), + outType, intent); + if (!mTransform) { + MOZ_LOG(sBMPLog, LogLevel::Debug, + ("failed to create color profile transform\n")); + } +} + +LexerTransition nsBMPDecoder::SeekColorProfile( + size_t aLength) { + // The offset needs to be at least after the color table. + uint32_t offset = mH.mColorSpace.mProfile.mOffset; + if (offset <= mH.mBIHSize + aLength + mNumColors * mBytesPerColor || + mH.mColorSpace.mProfile.mLength == 0) { + return Transition::To(State::ALLOCATE_SURFACE, 0); + } + + // We have already read the header and bitfields. + offset -= mH.mBIHSize + aLength; + + // We need to skip ahead to search for the embedded color profile. We want + // to return to this point once we read it. + mReturnIterator = mLexer.Clone(*mIterator, SIZE_MAX); + if (!mReturnIterator) { + return Transition::TerminateFailure(); + } + + return Transition::ToUnbuffered(State::FOUND_COLOR_PROFILE, + State::SKIP_TO_COLOR_PROFILE, offset); +} + +LexerTransition nsBMPDecoder::ReadColorProfile( + const char* aData, size_t aLength) { + mInProfile = qcms_profile_from_memory(aData, aLength); + if (mInProfile) { + MOZ_LOG(sBMPLog, LogLevel::Debug, ("using embedded color profile\n")); + PrepareColorProfileTransform(); + } + + // Jump back to where we left off. + mIterator = std::move(mReturnIterator); + return Transition::To(State::ALLOCATE_SURFACE, 0); +} + +LexerTransition nsBMPDecoder::AllocateSurface() { + SurfaceFormat format; + SurfacePipeFlags pipeFlags = SurfacePipeFlags(); + + if (mMayHaveTransparency) { + format = SurfaceFormat::OS_RGBA; + if (!(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA)) { + pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA; + } + } else { + format = SurfaceFormat::OS_RGBX; + } + + if (mH.mHeight >= 0) { + // BMPs store their rows in reverse order, so we may need to flip. + pipeFlags |= SurfacePipeFlags::FLIP_VERTICALLY; + } + + mRowBuffer.reset(new (fallible) uint32_t[mH.mWidth]); + if (!mRowBuffer) { + return Transition::TerminateFailure(); + } + + // Only give the color transform to the SurfacePipe if we are not transforming + // the color table in advance. + qcms_transform* transform = mColors ? nullptr : mTransform; + + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), FullFrame(), format, format, Nothing(), + transform, pipeFlags); + if (!pipe) { + return Transition::TerminateFailure(); + } + + mPipe = std::move(*pipe); + ClearRowBufferRemainder(); + return Transition::To(State::COLOR_TABLE, mNumColors * mBytesPerColor); +} + +LexerTransition nsBMPDecoder::ReadColorTable( + const char* aData, size_t aLength) { + MOZ_ASSERT_IF(aLength != 0, mNumColors > 0 && mColors); + + mPreGapLength += aLength; + + for (uint32_t i = 0; i < mNumColors; i++) { + // The format is BGR or BGR0. + mColors[i].mBlue = uint8_t(aData[0]); + mColors[i].mGreen = uint8_t(aData[1]); + mColors[i].mRed = uint8_t(aData[2]); + aData += mBytesPerColor; + } + + // If we have a color table and a transform, we can avoid transforming each + // pixel by doing the table in advance. We color manage every entry in the + // table, even if it is smaller in case the BMP is malformed and overruns + // its stated color range. + if (mColors && mTransform) { + qcms_transform_data(mTransform, mColors.get(), mColors.get(), 256); + } + + // If we are decoding a BMP from the clipboard, we did not know the data + // offset in advance. It is just defined as after the header and color table. + if (mIsForClipboard) { + mH.mDataOffset += mPreGapLength; + } + + // We know how many bytes we've read so far (mPreGapLength) and we know the + // offset of the pixel data (mH.mDataOffset), so we can determine the length + // of the gap (possibly zero) between the color table and the pixel data. + // + // If the gap is negative the file must be malformed (e.g. mH.mDataOffset + // points into the middle of the color palette instead of past the end) and + // we give up. + if (mPreGapLength > mH.mDataOffset) { + return Transition::TerminateFailure(); + } + + uint32_t gapLength = mH.mDataOffset - mPreGapLength; + + return Transition::ToUnbuffered(State::AFTER_GAP, State::GAP, gapLength); +} + +LexerTransition nsBMPDecoder::SkipGap() { + return Transition::ContinueUnbuffered(State::GAP); +} + +LexerTransition nsBMPDecoder::AfterGap() { + // If there are no pixels we can stop. + // + // XXX: normally, if there are no pixels we will have stopped decoding before + // now, outside of this decoder. However, if the BMP is within an ICO file, + // it's possible that the ICO claimed the image had a non-zero size while the + // BMP claims otherwise. This test is to catch that awkward case. If we ever + // come up with a more general solution to this ICO-and-BMP-disagree-on-size + // problem, this test can be removed. + if (mH.mWidth == 0 || mH.mHeight == 0) { + return Transition::TerminateSuccess(); + } + + bool hasRLE = mH.mCompression == Compression::RLE8 || + mH.mCompression == Compression::RLE4; + return hasRLE ? Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH) + : Transition::To(State::PIXEL_ROW, mPixelRowSize); +} + +LexerTransition nsBMPDecoder::ReadPixelRow( + const char* aData) { + MOZ_ASSERT(mCurrentRow > 0); + MOZ_ASSERT(mCurrentPos == 0); + + const uint8_t* src = reinterpret_cast(aData); + uint32_t* dst = RowBuffer(); + uint32_t lpos = mH.mWidth; + switch (mH.mBpp) { + case 1: + while (lpos > 0) { + int8_t bit; + uint8_t idx; + for (bit = 7; bit >= 0 && lpos > 0; bit--) { + idx = (*src >> bit) & 1; + SetPixel(dst, idx, mColors); + --lpos; + } + ++src; + } + break; + + case 4: + while (lpos > 0) { + Set4BitPixel(dst, *src, lpos, mColors); + ++src; + } + break; + + case 8: + while (lpos > 0) { + SetPixel(dst, *src, mColors); + --lpos; + ++src; + } + break; + + case 16: + if (mBitFields.IsR5G5B5()) { + // Specialize this common case. + while (lpos > 0) { + uint16_t val = LittleEndian::readUint16(src); + SetPixel(dst, mBitFields.mRed.Get5(val), mBitFields.mGreen.Get5(val), + mBitFields.mBlue.Get5(val)); + --lpos; + src += 2; + } + } else { + bool anyHasAlpha = false; + while (lpos > 0) { + uint16_t val = LittleEndian::readUint16(src); + SetPixel(dst, mBitFields.mRed.Get(val), mBitFields.mGreen.Get(val), + mBitFields.mBlue.Get(val), + mBitFields.mAlpha.GetAlpha(val, anyHasAlpha)); + --lpos; + src += 2; + } + if (anyHasAlpha) { + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + } + break; + + case 24: + while (lpos > 0) { + SetPixel(dst, src[2], src[1], src[0]); + --lpos; + src += 3; + } + break; + + case 32: + if (mH.mCompression == Compression::RGB && mIsWithinICO && + mH.mBpp == 32) { + // This is a special case only used for 32bpp WinBMPv3-ICO files, which + // could be in either 0RGB or ARGB format. We start by assuming it's + // an 0RGB image. If we hit a non-zero alpha value, then we know it's + // actually an ARGB image, and change tack accordingly. + // (Note: a fully-transparent ARGB image is indistinguishable from a + // 0RGB image, and we will render such an image as a 0RGB image, i.e. + // opaquely. This is unlikely to be a problem in practice.) + while (lpos > 0) { + if (!mDoesHaveTransparency && src[3] != 0) { + // Up until now this looked like an 0RGB image, but we now know + // it's actually an ARGB image. Which means every pixel we've seen + // so far has been fully transparent. So we go back and redo them. + + // Tell the SurfacePipe to go back to the start. + mPipe.ResetToFirstRow(); + + // Redo the complete rows we've already done. + MOZ_ASSERT(mCurrentPos == 0); + int32_t currentRow = mCurrentRow; + mCurrentRow = AbsoluteHeight(); + ClearRowBufferRemainder(); + while (mCurrentRow > currentRow) { + FinishRow(); + } + + // Reset the row pointer back to where we started. + dst = RowBuffer() + (mH.mWidth - lpos); + + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + + // If mDoesHaveTransparency is false, treat this as an 0RGB image. + // Otherwise, treat this as an ARGB image. + SetPixel(dst, src[2], src[1], src[0], + mDoesHaveTransparency ? src[3] : 0xff); + src += 4; + --lpos; + } + } else if (mBitFields.IsR8G8B8()) { + // Specialize this common case. + while (lpos > 0) { + uint32_t val = LittleEndian::readUint32(src); + SetPixel(dst, mBitFields.mRed.Get8(val), mBitFields.mGreen.Get8(val), + mBitFields.mBlue.Get8(val)); + --lpos; + src += 4; + } + } else { + bool anyHasAlpha = false; + while (lpos > 0) { + uint32_t val = LittleEndian::readUint32(src); + SetPixel(dst, mBitFields.mRed.Get(val), mBitFields.mGreen.Get(val), + mBitFields.mBlue.Get(val), + mBitFields.mAlpha.GetAlpha(val, anyHasAlpha)); + --lpos; + src += 4; + } + if (anyHasAlpha) { + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + } + break; + + default: + MOZ_CRASH("Unsupported color depth; earlier check didn't catch it?"); + } + + FinishRow(); + return mCurrentRow == 0 ? Transition::TerminateSuccess() + : Transition::To(State::PIXEL_ROW, mPixelRowSize); +} + +LexerTransition nsBMPDecoder::ReadRLESegment( + const char* aData) { + if (mCurrentRow == 0) { + return Transition::TerminateSuccess(); + } + + uint8_t byte1 = uint8_t(aData[0]); + uint8_t byte2 = uint8_t(aData[1]); + + if (byte1 != RLE::ESCAPE) { + // Encoded mode consists of two bytes: byte1 specifies the number of + // consecutive pixels to be drawn using the color index contained in + // byte2. + // + // Work around bitmaps that specify too many pixels. + uint32_t pixelsNeeded = std::min(mH.mWidth - mCurrentPos, byte1); + if (pixelsNeeded) { + uint32_t* dst = RowBuffer(); + mCurrentPos += pixelsNeeded; + if (mH.mCompression == Compression::RLE8) { + do { + SetPixel(dst, byte2, mColors); + pixelsNeeded--; + } while (pixelsNeeded); + } else { + do { + Set4BitPixel(dst, byte2, pixelsNeeded, mColors); + } while (pixelsNeeded); + } + } + return Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); + } + + if (byte2 == RLE::ESCAPE_EOL) { + ClearRowBufferRemainder(); + mCurrentPos = 0; + FinishRow(); + return mCurrentRow == 0 + ? Transition::TerminateSuccess() + : Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); + } + + if (byte2 == RLE::ESCAPE_EOF) { + return Transition::TerminateSuccess(); + } + + if (byte2 == RLE::ESCAPE_DELTA) { + return Transition::To(State::RLE_DELTA, RLE::DELTA_LENGTH); + } + + // Absolute mode. |byte2| gives the number of pixels. The length depends on + // whether it's 4-bit or 8-bit RLE. Also, the length must be even (and zero + // padding is used to achieve this when necessary). + MOZ_ASSERT(mAbsoluteModeNumPixels == 0); + mAbsoluteModeNumPixels = byte2; + uint32_t length = byte2; + if (mH.mCompression == Compression::RLE4) { + length = (length + 1) / 2; // halve, rounding up + } + if (length & 1) { + length++; + } + return Transition::To(State::RLE_ABSOLUTE, length); +} + +LexerTransition nsBMPDecoder::ReadRLEDelta( + const char* aData) { + // Delta encoding makes it possible to skip pixels making part of the image + // transparent. + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + + // Clear the skipped pixels. (This clears to the end of the row, + // which is perfect if there's a Y delta and harmless if not). + ClearRowBufferRemainder(); + + // Handle the XDelta. + mCurrentPos += uint8_t(aData[0]); + if (mCurrentPos > mH.mWidth) { + mCurrentPos = mH.mWidth; + } + + // Handle the Y Delta. + int32_t yDelta = std::min(uint8_t(aData[1]), mCurrentRow); + if (yDelta > 0) { + // Commit the current row (the first of the skipped rows). + FinishRow(); + + // Clear and commit the remaining skipped rows. We want to be careful not + // to change mCurrentPos here. + memset(mRowBuffer.get(), 0, mH.mWidth * sizeof(uint32_t)); + for (int32_t line = 1; line < yDelta; line++) { + FinishRow(); + } + } + + return mCurrentRow == 0 + ? Transition::TerminateSuccess() + : Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); +} + +LexerTransition nsBMPDecoder::ReadRLEAbsolute( + const char* aData, size_t aLength) { + uint32_t n = mAbsoluteModeNumPixels; + mAbsoluteModeNumPixels = 0; + + if (mCurrentPos + n > uint32_t(mH.mWidth)) { + // Some DIB RLE8 encoders count a padding byte as the absolute mode + // pixel number at the end of the row. + if (mH.mCompression == Compression::RLE8 && n > 0 && (n & 1) == 0 && + mCurrentPos + n - uint32_t(mH.mWidth) == 1 && aLength > 0 && + aData[aLength - 1] == 0) { + n--; + } else { + // Bad data. Stop decoding; at least part of the image may have been + // decoded. + return Transition::TerminateSuccess(); + } + } + + // In absolute mode, n represents the number of pixels that follow, each of + // which contains the color index of a single pixel. + uint32_t* dst = RowBuffer(); + uint32_t iSrc = 0; + uint32_t* oldPos = dst; + if (mH.mCompression == Compression::RLE8) { + while (n > 0) { + SetPixel(dst, aData[iSrc], mColors); + n--; + iSrc++; + } + } else { + while (n > 0) { + Set4BitPixel(dst, aData[iSrc], n, mColors); + iSrc++; + } + } + mCurrentPos += dst - oldPos; + + // We should read all the data (unless the last byte is zero padding). + MOZ_ASSERT(iSrc == aLength - 1 || iSrc == aLength); + + return Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsBMPDecoder.h b/image/decoders/nsBMPDecoder.h new file mode 100644 index 0000000000..c7990834b9 --- /dev/null +++ b/image/decoders/nsBMPDecoder.h @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsBMPDecoder_h +#define mozilla_image_decoders_nsBMPDecoder_h + +#include "BMPHeaders.h" +#include "Decoder.h" +#include "gfxColor.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace image { + +namespace bmp { + +struct CalRgbEndpoint { + uint32_t mGamma; + uint32_t mX; + uint32_t mY; + uint32_t mZ; +}; + +/// This struct contains the fields from the file header and info header that +/// we use during decoding. (Excluding bitfields fields, which are kept in +/// BitFields.) +struct Header { + uint32_t mDataOffset; // Offset to raster data. + uint32_t mBIHSize; // Header size. + int32_t mWidth; // Image width. + int32_t mHeight; // Image height. + uint16_t mBpp; // Bits per pixel. + uint32_t mCompression; // See struct Compression for valid values. + uint32_t mImageSize; // (compressed) image size. Can be 0 if + // mCompression==0. + uint32_t mNumColors; // Used colors. + InfoColorSpace mCsType; // Color space type. + InfoColorIntent mCsIntent; // Color space intent. + + union { + struct { + CalRgbEndpoint mRed; + CalRgbEndpoint mGreen; + CalRgbEndpoint mBlue; + } mCalibrated; + + struct { + uint32_t mOffset; + uint32_t mLength; + } mProfile; + } mColorSpace; + + Header() + : mDataOffset(0), + mBIHSize(0), + mWidth(0), + mHeight(0), + mBpp(0), + mCompression(0), + mImageSize(0), + mNumColors(0), + mCsType(InfoColorSpace::SRGB), + mCsIntent(InfoColorIntent::IMAGES) {} +}; + +/// An entry in the color table. +struct ColorTableEntry { + uint8_t mRed; + uint8_t mGreen; + uint8_t mBlue; +}; + +/// All the color-related bitfields for 16bpp and 32bpp images. We use this +/// even for older format BMPs that don't have explicit bitfields. +class BitFields { + class Value { + friend class BitFields; + + uint32_t mMask; // The mask for the value. + uint8_t mRightShift; // The amount to right-shift after masking. + uint8_t mBitWidth; // The width (in bits) of the value. + + /// Sets the mask (and thus the right-shift and bit-width as well). + void Set(uint32_t aMask); + + public: + Value() { + mMask = 0; + mRightShift = 0; + mBitWidth = 0; + } + + /// Returns true if this channel is used. Only used for alpha. + bool IsPresent() const { return mMask != 0x0; } + + /// Extracts the single color value from the multi-color value. + uint8_t Get(uint32_t aVal) const; + + /// Like Get(), but specially for alpha. + uint8_t GetAlpha(uint32_t aVal, bool& aHasAlphaOut) const; + + /// Specialized versions of Get() for when the bit-width is 5 or 8. + /// (They will assert if called and the bit-width is not 5 or 8.) + uint8_t Get5(uint32_t aVal) const; + uint8_t Get8(uint32_t aVal) const; + }; + + public: + /// The individual color channels. + Value mRed; + Value mGreen; + Value mBlue; + Value mAlpha; + + /// Set bitfields to the standard 5-5-5 16bpp values. + void SetR5G5B5(); + + /// Set bitfields to the standard 8-8-8 32bpp values. + void SetR8G8B8(); + + /// Test if bitfields have the standard 5-5-5 16bpp values. + bool IsR5G5B5() const; + + /// Test if bitfields have the standard 8-8-8 32bpp values. + bool IsR8G8B8() const; + + /// Read the bitfields from a header. The reading of the alpha mask is + /// optional. + void ReadFromHeader(const char* aData, bool aReadAlpha); + + /// Length of the bitfields structure in the BMP file. + static const size_t LENGTH = 12; +}; + +} // namespace bmp + +class RasterImage; + +/// Decoder for BMP-Files, as used by Windows and OS/2. + +class nsBMPDecoder : public Decoder { + public: + ~nsBMPDecoder(); + + DecoderType GetType() const override { return DecoderType::BMP; } + + /// @return true if this BMP is a valid ICO resource. + bool IsValidICOResource() const override { return true; } + + /// Obtains the internal output image buffer. + uint32_t* GetImageData() { return reinterpret_cast(mImageData); } + + /// Obtains the length of the internal output image buffer. + size_t GetImageDataLength() const { return mImageDataLength; } + + /// Obtains the size of the compressed image resource. + int32_t GetCompressedImageSize() const; + + /// Mark this BMP as being within an ICO file. Only used for testing purposes + /// because the ICO-specific constructor does this marking automatically. + void SetIsWithinICO() { mIsWithinICO = true; } + + /// Did the BMP file have alpha data of any kind? (Only use this after the + /// bitmap has been fully decoded.) + bool HasTransparency() const { return mDoesHaveTransparency; } + + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult BeforeFinishInternal() override; + nsresult FinishInternal() override; + + private: + friend class DecoderFactory; + + enum class State { + FILE_HEADER, + INFO_HEADER_SIZE, + INFO_HEADER_REST, + BITFIELDS, + SKIP_TO_COLOR_PROFILE, + FOUND_COLOR_PROFILE, + COLOR_PROFILE, + ALLOCATE_SURFACE, + COLOR_TABLE, + GAP, + AFTER_GAP, + PIXEL_ROW, + RLE_SEGMENT, + RLE_DELTA, + RLE_ABSOLUTE + }; + + // This is the constructor used for normal and clipboard BMP images. + explicit nsBMPDecoder(RasterImage* aImage, bool aForClipboard = false); + + // This is the constructor used for BMP resources in ICO images. + nsBMPDecoder(RasterImage* aImage, uint32_t aDataOffset); + + // Helper constructor called by the other two. + nsBMPDecoder(RasterImage* aImage, State aState, size_t aLength, + bool aForClipboard); + + int32_t AbsoluteHeight() const { return abs(mH.mHeight); } + + uint32_t* RowBuffer(); + void ClearRowBufferRemainder(); + + void FinishRow(); + + void PrepareCalibratedColorProfile(); + void PrepareColorProfileTransform(); + + LexerTransition ReadFileHeader(const char* aData, size_t aLength); + LexerTransition ReadInfoHeaderSize(const char* aData, size_t aLength); + LexerTransition ReadInfoHeaderRest(const char* aData, size_t aLength); + LexerTransition ReadBitfields(const char* aData, size_t aLength); + LexerTransition SeekColorProfile(size_t aLength); + LexerTransition ReadColorProfile(const char* aData, size_t aLength); + LexerTransition AllocateSurface(); + LexerTransition ReadColorTable(const char* aData, size_t aLength); + LexerTransition SkipGap(); + LexerTransition AfterGap(); + LexerTransition ReadPixelRow(const char* aData); + LexerTransition ReadRLESegment(const char* aData); + LexerTransition ReadRLEDelta(const char* aData); + LexerTransition ReadRLEAbsolute(const char* aData, size_t aLength); + + SurfacePipe mPipe; + + StreamingLexer mLexer; + + // Iterator to save return point. + Maybe mReturnIterator; + + UniquePtr mRowBuffer; + + bmp::Header mH; + + // If the BMP is within an ICO file our treatment of it differs slightly. + bool mIsWithinICO; + + // If the BMP decoded from the clipboard, we don't start with a data offset. + bool mIsForClipboard; + + bmp::BitFields mBitFields; + + // Might the image have transparency? Determined from the headers during + // metadata decode. (Does not guarantee the image actually has transparency.) + bool mMayHaveTransparency; + + // Does the image have transparency? Determined during full decoding, so only + // use this after that has been completed. + bool mDoesHaveTransparency; + + uint32_t mNumColors; // The number of used colors, i.e. the number of + // entries in mColors, if it's present. + UniquePtr + mColors; // The color table, if it's present. + uint32_t mBytesPerColor; // 3 or 4, depending on the format + + // The number of bytes prior to the optional gap that have been read. This + // is used to find the start of the pixel data. + uint32_t mPreGapLength; + + uint32_t mPixelRowSize; // The number of bytes per pixel row. + + int32_t mCurrentRow; // Index of the row of the image that's currently + // being decoded: [height,1]. + int32_t mCurrentPos; // Index into the current line. Used when + // doing RLE decoding and when filling in pixels + // for truncated files. + + // Only used in RLE_ABSOLUTE state: the number of pixels to read. + uint32_t mAbsoluteModeNumPixels; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsBMPDecoder_h diff --git a/image/decoders/nsGIFDecoder2.cpp b/image/decoders/nsGIFDecoder2.cpp new file mode 100644 index 0000000000..9b2de9124a --- /dev/null +++ b/image/decoders/nsGIFDecoder2.cpp @@ -0,0 +1,1073 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* +The Graphics Interchange Format(c) is the copyright property of CompuServe +Incorporated. Only CompuServe Incorporated is authorized to define, redefine, +enhance, alter, modify or change in any way the definition of the format. + +CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free +license for the use of the Graphics Interchange Format(sm) in computer +software; computer software utilizing GIF(sm) must acknowledge ownership of the +Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in +User and Technical Documentation. Computer software utilizing GIF, which is +distributed or may be distributed without User or Technical Documentation must +display to the screen or printer a message acknowledging ownership of the +Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in +this case, the acknowledgement may be displayed in an opening screen or leading +banner, or a closing screen or trailing banner. A message such as the following +may be used: + + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +For further information, please contact : + + CompuServe Incorporated + Graphics Technology Department + 5000 Arlington Center Boulevard + Columbus, Ohio 43220 + U. S. A. + +CompuServe Incorporated maintains a mailing list with all those individuals and +organizations who wish to receive copies of this document when it is corrected +or revised. This service is offered free of charge; please provide us with your +mailing address. +*/ + +#include "nsGIFDecoder2.h" + +#include + +#include "imgFrame.h" +#include "mozilla/EndianUtils.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "qcms.h" +#include +#include "mozilla/Telemetry.h" + +using namespace mozilla::gfx; + +using std::max; + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////// +// GIF Decoder Implementation + +static const size_t GIF_HEADER_LEN = 6; +static const size_t GIF_SCREEN_DESCRIPTOR_LEN = 7; +static const size_t BLOCK_HEADER_LEN = 1; +static const size_t SUB_BLOCK_HEADER_LEN = 1; +static const size_t EXTENSION_HEADER_LEN = 2; +static const size_t GRAPHIC_CONTROL_EXTENSION_LEN = 4; +static const size_t APPLICATION_EXTENSION_LEN = 11; +static const size_t IMAGE_DESCRIPTOR_LEN = 9; + +// Masks for reading color table information from packed fields in the screen +// descriptor and image descriptor blocks. +static const uint8_t PACKED_FIELDS_COLOR_TABLE_BIT = 0x80; +static const uint8_t PACKED_FIELDS_INTERLACED_BIT = 0x40; +static const uint8_t PACKED_FIELDS_TABLE_DEPTH_MASK = 0x07; + +nsGIFDecoder2::nsGIFDecoder2(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::To(State::GIF_HEADER, GIF_HEADER_LEN), + Transition::TerminateSuccess()), + mOldColor(0), + mCurrentFrameIndex(-1), + mColorTablePos(0), + mColormap(nullptr), + mColormapSize(0), + mColorMask('\0'), + mGIFOpen(false), + mSawTransparency(false), + mSwizzleFn(nullptr) { + // Clear out the structure, excluding the arrays. Ensure that the global + // colormap is initialized as opaque. + memset(&mGIFStruct, 0, sizeof(mGIFStruct)); + memset(mGIFStruct.global_colormap, 0xFF, sizeof(mGIFStruct.global_colormap)); + + // Each color table will need to be unpacked. + mSwizzleFn = SwizzleRow(SurfaceFormat::R8G8B8, SurfaceFormat::OS_RGBA); + MOZ_ASSERT(mSwizzleFn); +} + +nsGIFDecoder2::~nsGIFDecoder2() { free(mGIFStruct.local_colormap); } + +nsresult nsGIFDecoder2::FinishInternal() { + MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); + + // If the GIF got cut off, handle it anyway + if (!IsMetadataDecode() && mGIFOpen) { + if (mCurrentFrameIndex == mGIFStruct.images_decoded) { + EndImageFrame(); + } + PostDecodeDone(mGIFStruct.loop_count); + mGIFOpen = false; + } + + return NS_OK; +} + +void nsGIFDecoder2::FlushImageData() { + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (!invalidRect) { + return; + } + + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); +} + +//****************************************************************************** +// GIF decoder callback methods. Part of public API for GIF2 +//****************************************************************************** + +//****************************************************************************** +void nsGIFDecoder2::BeginGIF() { + if (mGIFOpen) { + return; + } + + mGIFOpen = true; + + PostSize(mGIFStruct.screen_width, mGIFStruct.screen_height); +} + +bool nsGIFDecoder2::CheckForTransparency(const OrientedIntRect& aFrameRect) { + // Check if the image has a transparent color in its palette. + if (mGIFStruct.is_transparent) { + PostHasTransparency(); + return true; + } + + // This is a bit of a hack. Some sites will use a 1x1 gif that includes no + // header information indicating it is transparent, no palette, and no image + // data at all (so no pixels get written) to represent a transparent pixel + // using the absolute least number of bytes. Generally things are setup to + // detect transparency without decoding the image data. So to detect this kind + // of transparency without decoing the image data we would have to assume + // every gif is transparent, which we would like to avoid. Changing things so + // that we can detect transparency at any point of decoding is a bigger change + // and not worth it for one questionable 1x1 gif. Using this "trick" for + // anything but 1x1 transparent spacer gifs doesn't make sense, so it's + // reasonable to target 1x1 gifs just for this. + if (mGIFStruct.screen_width == 1 && mGIFStruct.screen_height == 1) { + PostHasTransparency(); + return true; + } + + if (mGIFStruct.images_decoded > 0) { + return false; // We only care about first frame padding below. + } + + // If we need padding on the first frame, that means we don't draw into part + // of the image at all. Report that as transparency. + OrientedIntRect imageRect(0, 0, mGIFStruct.screen_width, + mGIFStruct.screen_height); + if (!imageRect.IsEqualEdges(aFrameRect)) { + PostHasTransparency(); + mSawTransparency = true; // Make sure we don't optimize it away. + return true; + } + + return false; +} + +//****************************************************************************** +nsresult nsGIFDecoder2::BeginImageFrame(const OrientedIntRect& aFrameRect, + uint16_t aDepth, bool aIsInterlaced) { + MOZ_ASSERT(HasSize()); + + bool hasTransparency = CheckForTransparency(aFrameRect); + + // Make sure there's no animation if we're downscaling. + MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); + + Maybe animParams; + if (!IsFirstFrameDecode()) { + animParams.emplace(aFrameRect.ToUnknownRect(), + FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time), + uint32_t(mGIFStruct.images_decoded), BlendMethod::OVER, + DisposalMethod(mGIFStruct.disposal_method)); + } + + SurfacePipeFlags pipeFlags = + aIsInterlaced ? SurfacePipeFlags::DEINTERLACE : SurfacePipeFlags(); + + gfx::SurfaceFormat format; + if (mGIFStruct.images_decoded == 0) { + // The first frame may be displayed progressively. + pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + + // Only allow opaque surfaces if we are decoding a single image without + // transparency. For an animation, there isn't much benefit to RGBX given + // the current frame is constantly changing, and there are many risks + // since BlendAnimationFilter is able to clear rows of data. + format = hasTransparency || animParams ? SurfaceFormat::OS_RGBA + : SurfaceFormat::OS_RGBX; + } else { + format = SurfaceFormat::OS_RGBA; + } + + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), aFrameRect, format, format, animParams, + mTransform, pipeFlags); + mCurrentFrameIndex = mGIFStruct.images_decoded; + + if (!pipe) { + mPipe = SurfacePipe(); + return NS_ERROR_FAILURE; + } + + mPipe = std::move(*pipe); + return NS_OK; +} + +//****************************************************************************** +void nsGIFDecoder2::EndImageFrame() { + Opacity opacity = Opacity::SOME_TRANSPARENCY; + + if (mGIFStruct.images_decoded == 0) { + // We need to send invalidations for the first frame. + FlushImageData(); + + // The first frame was preallocated with alpha; if it wasn't transparent, we + // should fix that. We can also mark it opaque unconditionally if we didn't + // actually see any transparent pixels - this test is only valid for the + // first frame. + if (!mGIFStruct.is_transparent && !mSawTransparency) { + opacity = Opacity::FULLY_OPAQUE; + } + } + + // Unconditionally increment images_decoded, because we unconditionally + // append frames in BeginImageFrame(). This ensures that images_decoded + // always refers to the frame in mImage we're currently decoding, + // even if some of them weren't decoded properly and thus are blank. + mGIFStruct.images_decoded++; + + // Reset graphic control extension parameters that we shouldn't reuse + // between frames. + mGIFStruct.delay_time = 0; + + // Tell the superclass we finished a frame + PostFrameStop(opacity); + + // Reset the transparent pixel + if (mOldColor) { + mColormap[mGIFStruct.tpixel] = mOldColor; + mOldColor = 0; + } + + mColormap = nullptr; + mColormapSize = 0; + mCurrentFrameIndex = -1; +} + +template +PixelSize nsGIFDecoder2::ColormapIndexToPixel(uint8_t aIndex) { + MOZ_ASSERT(sizeof(PixelSize) == sizeof(uint32_t)); + + // Retrieve the next color, clamping to the size of the colormap. + uint32_t color = mColormap[aIndex & mColorMask]; + + // Check for transparency. + if (mGIFStruct.is_transparent) { + mSawTransparency = mSawTransparency || color == 0; + } + + return color; +} + +template <> +uint8_t nsGIFDecoder2::ColormapIndexToPixel(uint8_t aIndex) { + return aIndex & mColorMask; +} + +template +std::tuple> nsGIFDecoder2::YieldPixels( + const uint8_t* aData, size_t aLength, size_t* aBytesReadOut, + PixelSize* aPixelBlock, int32_t aBlockSize) { + MOZ_ASSERT(aData); + MOZ_ASSERT(aBytesReadOut); + MOZ_ASSERT(mGIFStruct.stackp >= mGIFStruct.stack); + + // Advance to the next byte we should read. + const uint8_t* data = aData + *aBytesReadOut; + + int32_t written = 0; + while (aBlockSize > written) { + // If we don't have any decoded data to yield, try to read some input and + // produce some. + if (mGIFStruct.stackp == mGIFStruct.stack) { + while (mGIFStruct.bits < mGIFStruct.codesize && + *aBytesReadOut < aLength) { + // Feed the next byte into the decoder's 32-bit input buffer. + mGIFStruct.datum += int32_t(*data) << mGIFStruct.bits; + mGIFStruct.bits += 8; + data += 1; + *aBytesReadOut += 1; + } + + if (mGIFStruct.bits < mGIFStruct.codesize) { + return std::make_tuple(written, Some(WriteState::NEED_MORE_DATA)); + } + + // Get the leading variable-length symbol from the data stream. + int code = mGIFStruct.datum & mGIFStruct.codemask; + mGIFStruct.datum >>= mGIFStruct.codesize; + mGIFStruct.bits -= mGIFStruct.codesize; + + const int clearCode = ClearCode(); + + // Reset the dictionary to its original state, if requested + if (code == clearCode) { + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + return std::make_tuple(written, Some(WriteState::NEED_MORE_DATA)); + } + + // Check for explicit end-of-stream code. It should only appear after all + // image data, but if that was the case we wouldn't be in this function, + // so this is always an error condition. + if (code == (clearCode + 1)) { + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + if (mGIFStruct.oldcode == -1) { + if (code >= MAX_BITS) { + // The code's too big; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + mGIFStruct.firstchar = mGIFStruct.oldcode = code; + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + aPixelBlock[written++] = + ColormapIndexToPixel(mGIFStruct.suffix[code]); + continue; + } + + int incode = code; + if (code >= mGIFStruct.avail) { + *mGIFStruct.stackp++ = mGIFStruct.firstchar; + code = mGIFStruct.oldcode; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + // Stack overflow; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + } + + while (code >= clearCode) { + if ((code >= MAX_BITS) || (code == mGIFStruct.prefix[code])) { + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + *mGIFStruct.stackp++ = mGIFStruct.suffix[code]; + code = mGIFStruct.prefix[code]; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + // Stack overflow; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + } + + *mGIFStruct.stackp++ = mGIFStruct.firstchar = mGIFStruct.suffix[code]; + + // Define a new codeword in the dictionary. + if (mGIFStruct.avail < 4096) { + mGIFStruct.prefix[mGIFStruct.avail] = mGIFStruct.oldcode; + mGIFStruct.suffix[mGIFStruct.avail] = mGIFStruct.firstchar; + mGIFStruct.avail++; + + // If we've used up all the codewords of a given length increase the + // length of codewords by one bit, but don't exceed the specified + // maximum codeword size of 12 bits. + if (((mGIFStruct.avail & mGIFStruct.codemask) == 0) && + (mGIFStruct.avail < 4096)) { + mGIFStruct.codesize++; + mGIFStruct.codemask += mGIFStruct.avail; + } + } + + mGIFStruct.oldcode = incode; + } + + if (MOZ_UNLIKELY(mGIFStruct.stackp <= mGIFStruct.stack)) { + MOZ_ASSERT_UNREACHABLE("No decoded data but we didn't return early?"); + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + aPixelBlock[written++] = + ColormapIndexToPixel(*--mGIFStruct.stackp); + } + + return std::make_tuple(written, Maybe()); +} + +/// Expand the colormap from RGB to Packed ARGB as needed by Cairo. +/// And apply any LCMS transformation. +void nsGIFDecoder2::ConvertColormap(uint32_t* aColormap, uint32_t aColors) { + if (!aColors) { + return; + } + + // Apply CMS transformation if enabled and available + if (mCMSMode == CMSMode::All) { + qcms_transform* transform = GetCMSsRGBTransform(SurfaceFormat::R8G8B8); + if (transform) { + qcms_transform_data(transform, aColormap, aColormap, aColors); + } + } + + // Expand color table from RGB to BGRA. + MOZ_ASSERT(mSwizzleFn); + uint8_t* data = reinterpret_cast(aColormap); + mSwizzleFn(data, data, aColors); +} + +LexerResult nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex( + aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::GIF_HEADER: + return ReadGIFHeader(aData); + case State::SCREEN_DESCRIPTOR: + return ReadScreenDescriptor(aData); + case State::GLOBAL_COLOR_TABLE: + return ReadGlobalColorTable(aData, aLength); + case State::FINISHED_GLOBAL_COLOR_TABLE: + return FinishedGlobalColorTable(); + case State::BLOCK_HEADER: + return ReadBlockHeader(aData); + case State::EXTENSION_HEADER: + return ReadExtensionHeader(aData); + case State::GRAPHIC_CONTROL_EXTENSION: + return ReadGraphicControlExtension(aData); + case State::APPLICATION_IDENTIFIER: + return ReadApplicationIdentifier(aData); + case State::NETSCAPE_EXTENSION_SUB_BLOCK: + return ReadNetscapeExtensionSubBlock(aData); + case State::NETSCAPE_EXTENSION_DATA: + return ReadNetscapeExtensionData(aData); + case State::IMAGE_DESCRIPTOR: + return ReadImageDescriptor(aData); + case State::FINISH_IMAGE_DESCRIPTOR: + return FinishImageDescriptor(aData); + case State::LOCAL_COLOR_TABLE: + return ReadLocalColorTable(aData, aLength); + case State::FINISHED_LOCAL_COLOR_TABLE: + return FinishedLocalColorTable(); + case State::IMAGE_DATA_BLOCK: + return ReadImageDataBlock(aData); + case State::IMAGE_DATA_SUB_BLOCK: + return ReadImageDataSubBlock(aData); + case State::LZW_DATA: + return ReadLZWData(aData, aLength); + case State::SKIP_LZW_DATA: + return Transition::ContinueUnbuffered(State::SKIP_LZW_DATA); + case State::FINISHED_LZW_DATA: + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + case State::SKIP_SUB_BLOCKS: + return SkipSubBlocks(aData); + case State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS: + return Transition::ContinueUnbuffered( + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS); + case State::FINISHED_SKIPPING_DATA: + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition nsGIFDecoder2::ReadGIFHeader( + const char* aData) { + // We retrieve the version here but because many GIF encoders set header + // fields incorrectly, we barely use it; features which should only appear in + // GIF89a are always accepted. + if (strncmp(aData, "GIF87a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 87; + } else if (strncmp(aData, "GIF89a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 89; + } else { + return Transition::TerminateFailure(); + } + + return Transition::To(State::SCREEN_DESCRIPTOR, GIF_SCREEN_DESCRIPTOR_LEN); +} + +LexerTransition nsGIFDecoder2::ReadScreenDescriptor( + const char* aData) { + mGIFStruct.screen_width = LittleEndian::readUint16(aData + 0); + mGIFStruct.screen_height = LittleEndian::readUint16(aData + 2); + + const uint8_t packedFields = aData[4]; + + // XXX: Should we be capturing these values even if there is no global color + // table? + mGIFStruct.global_colormap_depth = + (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + mGIFStruct.global_colormap_count = 1 << mGIFStruct.global_colormap_depth; + + // We ignore several fields in the header. We don't care about the 'sort + // flag', which indicates if the global color table's entries are sorted in + // order of importance - if we need to render this image for a device with a + // narrower color gamut than GIF supports we'll handle that at a different + // layer. We have no use for the pixel aspect ratio as well. Finally, we + // intentionally ignore the background color index, as implementing that + // feature would not be web compatible - when a GIF image frame doesn't cover + // the entire area of the image, the area that's not covered should always be + // transparent. + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + MOZ_ASSERT(mColorTablePos == 0); + + // We read the global color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + const size_t globalColorTableSize = 3 * mGIFStruct.global_colormap_count; + return Transition::ToUnbuffered(State::FINISHED_GLOBAL_COLOR_TABLE, + State::GLOBAL_COLOR_TABLE, + globalColorTableSize); + } + + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadGlobalColorTable( + const char* aData, size_t aLength) { + uint8_t* dest = + reinterpret_cast(mGIFStruct.global_colormap) + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::GLOBAL_COLOR_TABLE); +} + +LexerTransition +nsGIFDecoder2::FinishedGlobalColorTable() { + ConvertColormap(mGIFStruct.global_colormap, mGIFStruct.global_colormap_count); + mColorTablePos = 0; + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadBlockHeader( + const char* aData) { + // Determine what type of block we're dealing with. + switch (aData[0]) { + case GIF_EXTENSION_INTRODUCER: + return Transition::To(State::EXTENSION_HEADER, EXTENSION_HEADER_LEN); + + case GIF_IMAGE_SEPARATOR: + return Transition::To(State::IMAGE_DESCRIPTOR, IMAGE_DESCRIPTOR_LEN); + + case GIF_TRAILER: + FinishInternal(); + return Transition::TerminateSuccess(); + + default: + // If we get anything other than GIF_IMAGE_SEPARATOR, + // GIF_EXTENSION_INTRODUCER, or GIF_TRAILER, there is extraneous data + // between blocks. The GIF87a spec tells us to keep reading until we find + // an image separator, but GIF89a says such a file is corrupt. We follow + // GIF89a and bail out. + + if (mGIFStruct.images_decoded > 0) { + // The file is corrupt, but we successfully decoded some frames, so we + // may as well consider the decode successful and display them. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // No images decoded; there is nothing to display. + return Transition::TerminateFailure(); + } +} + +LexerTransition nsGIFDecoder2::ReadExtensionHeader( + const char* aData) { + const uint8_t label = aData[0]; + const uint8_t extensionHeaderLength = aData[1]; + + // If the extension header is zero length, just treat it as a block terminator + // and move on to the next block immediately. + if (extensionHeaderLength == 0) { + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + switch (label) { + case GIF_GRAPHIC_CONTROL_LABEL: + // The GIF spec mandates that the Control Extension header block length is + // 4 bytes, and the parser for this block reads 4 bytes, so we must + // enforce that the buffer contains at least this many bytes. If the GIF + // specifies a different length, we allow that, so long as it's larger; + // the additional data will simply be ignored. + return Transition::To( + State::GRAPHIC_CONTROL_EXTENSION, + max(extensionHeaderLength, GRAPHIC_CONTROL_EXTENSION_LEN)); + + case GIF_APPLICATION_EXTENSION_LABEL: + // Again, the spec specifies that an application extension header is 11 + // bytes, but for compatibility with GIFs in the wild, we allow deviation + // from the spec. This is important for real-world compatibility, as GIFs + // in the wild exist with application extension headers that are both + // shorter and longer than 11 bytes. However, we only try to actually + // interpret the application extension if the length is correct; + // otherwise, we just skip the block unconditionally. + return extensionHeaderLength == APPLICATION_EXTENSION_LEN + ? Transition::To(State::APPLICATION_IDENTIFIER, + extensionHeaderLength) + : Transition::ToUnbuffered( + State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + + default: + // Skip over any other type of extension block, including comment and + // plain text blocks. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + } +} + +LexerTransition +nsGIFDecoder2::ReadGraphicControlExtension(const char* aData) { + mGIFStruct.is_transparent = aData[0] & 0x1; + mGIFStruct.tpixel = uint8_t(aData[3]); + mGIFStruct.disposal_method = (aData[0] >> 2) & 0x7; + + if (mGIFStruct.disposal_method == 4) { + // Some encoders (and apparently some specs) represent + // DisposalMethod::RESTORE_PREVIOUS as 4, but 3 is used in the canonical + // spec and is more popular, so we normalize to 3. + mGIFStruct.disposal_method = 3; + } else if (mGIFStruct.disposal_method > 4) { + // This GIF is using a disposal method which is undefined in the spec. + // Treat it as DisposalMethod::NOT_SPECIFIED. + mGIFStruct.disposal_method = 0; + } + + DisposalMethod method = DisposalMethod(mGIFStruct.disposal_method); + if (method == DisposalMethod::CLEAR_ALL || method == DisposalMethod::CLEAR) { + // We may have to display the background under this image during animation + // playback, so we regard it as transparent. + PostHasTransparency(); + } + + mGIFStruct.delay_time = LittleEndian::readUint16(aData + 1) * 10; + if (!HasAnimation() && mGIFStruct.delay_time > 0) { + PostIsAnimated(FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time)); + } + + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadApplicationIdentifier( + const char* aData) { + if ((strncmp(aData, "NETSCAPE2.0", 11) == 0) || + (strncmp(aData, "ANIMEXTS1.0", 11) == 0)) { + // This is a Netscape application extension block. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + } + + // This is an application extension we don't care about. Just skip it. + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition +nsGIFDecoder2::ReadNetscapeExtensionSubBlock(const char* aData) { + const uint8_t blockLength = aData[0]; + if (blockLength == 0) { + // We hit the block terminator. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // We consume a minimum of 3 bytes in accordance with the specs for the + // Netscape application extension block, such as they are. + const size_t extensionLength = max(blockLength, 3); + return Transition::To(State::NETSCAPE_EXTENSION_DATA, extensionLength); +} + +LexerTransition nsGIFDecoder2::ReadNetscapeExtensionData( + const char* aData) { + // Documentation for NETSCAPE2.0 / ANIMEXTS1.0 extensions can be found at: + // https://wiki.whatwg.org/wiki/GIF + static const uint8_t NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID = 1; + static const uint8_t NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID = 2; + + const uint8_t subBlockID = aData[0] & 7; + switch (subBlockID) { + case NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID: + // This is looping extension. + mGIFStruct.loop_count = LittleEndian::readUint16(aData + 1); + // Zero loop count is infinite animation loop request. + if (mGIFStruct.loop_count == 0) { + mGIFStruct.loop_count = -1; + } + + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + case NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID: + // We allow, but ignore, this extension. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + default: + return Transition::TerminateFailure(); + } +} + +LexerTransition nsGIFDecoder2::ReadImageDescriptor( + const char* aData) { + // On the first frame, we don't need to yield, and none of the other checks + // below apply, so we can just jump right into FinishImageDescriptor(). + if (mGIFStruct.images_decoded == 0) { + return FinishImageDescriptor(aData); + } + + if (!HasAnimation()) { + // We should've already called PostIsAnimated(); this must be a corrupt + // animated image with a first frame timeout of zero. Signal that we're + // animated now, before the first-frame decode early exit below, so that + // RasterImage can detect that this happened. + PostIsAnimated(FrameTimeout::FromRawMilliseconds(0)); + } + + if (IsFirstFrameDecode()) { + // We're about to get a second frame, but we only want the first. Stop + // decoding now. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + MOZ_ASSERT(Size() == OutputSize(), "Downscaling an animated image?"); + + // Yield to allow access to the previous frame before we start a new one. + return Transition::ToAfterYield(State::FINISH_IMAGE_DESCRIPTOR); +} + +LexerTransition nsGIFDecoder2::FinishImageDescriptor( + const char* aData) { + OrientedIntRect frameRect; + + // Get image offsets with respect to the screen origin. + frameRect.SetRect( + LittleEndian::readUint16(aData + 0), LittleEndian::readUint16(aData + 2), + LittleEndian::readUint16(aData + 4), LittleEndian::readUint16(aData + 6)); + + if (!mGIFStruct.images_decoded) { + // Work around GIF files where + // * at least one of the logical screen dimensions is smaller than the + // same dimension in the first image, or + // * GIF87a files where the first image's dimensions do not match the + // logical screen dimensions. + if (mGIFStruct.screen_height < frameRect.Height() || + mGIFStruct.screen_width < frameRect.Width() || + mGIFStruct.version == 87) { + mGIFStruct.screen_height = frameRect.Height(); + mGIFStruct.screen_width = frameRect.Width(); + frameRect.MoveTo(0, 0); + } + + // Create the image container with the right size. + BeginGIF(); + if (HasError()) { + // Setting the size led to an error. + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + CheckForTransparency(frameRect); + FinishInternal(); + return Transition::TerminateSuccess(); + } + } + + // Work around broken GIF files that have zero frame width or height; in this + // case, we'll treat the frame as having the same size as the overall image. + if (frameRect.Height() == 0 || frameRect.Width() == 0) { + frameRect.SetHeight(mGIFStruct.screen_height); + frameRect.SetWidth(mGIFStruct.screen_width); + + // If that still resulted in zero frame width or height, give up. + if (frameRect.Height() == 0 || frameRect.Width() == 0) { + return Transition::TerminateFailure(); + } + } + + // Determine |depth| (log base 2 of the number of colors in the palette). + bool haveLocalColorTable = false; + uint16_t depth = 0; + uint8_t packedFields = aData[8]; + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + // Get the palette depth from the local color table. + depth = (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + haveLocalColorTable = true; + } else { + // Get the palette depth from the global color table. + depth = mGIFStruct.global_colormap_depth; + } + + // If the transparent color index is greater than the number of colors in the + // color table, we may need a higher color depth than |depth| would specify. + // Our internal representation of the image will instead use |realDepth|, + // which is the smallest color depth that can accommodate the existing palette + // *and* the transparent color index. + uint16_t realDepth = depth; + while (mGIFStruct.tpixel >= (1 << realDepth) && realDepth < 8) { + realDepth++; + } + + // Create a mask used to ensure that color values fit within the colormap. + mColorMask = 0xFF >> (8 - realDepth); + + // Determine if this frame is interlaced or not. + const bool isInterlaced = packedFields & PACKED_FIELDS_INTERLACED_BIT; + + // Create the SurfacePipe we'll use to write output for this frame. + if (NS_FAILED(BeginImageFrame(frameRect, realDepth, isInterlaced))) { + return Transition::TerminateFailure(); + } + + // Clear state from last image. + mGIFStruct.pixels_remaining = + int64_t(frameRect.Width()) * int64_t(frameRect.Height()); + + if (haveLocalColorTable) { + // We have a local color table, so prepare to read it into the palette of + // the current frame. + mGIFStruct.local_colormap_size = 1 << depth; + + if (!mColormap) { + // Ensure our current colormap buffer is large enough to hold the new one. + mColormapSize = sizeof(uint32_t) << realDepth; + if (mGIFStruct.local_colormap_buffer_size < mColormapSize) { + if (mGIFStruct.local_colormap) { + free(mGIFStruct.local_colormap); + } + mGIFStruct.local_colormap_buffer_size = mColormapSize; + mGIFStruct.local_colormap = + static_cast(moz_xmalloc(mColormapSize)); + // Ensure the local colormap is initialized as opaque. + memset(mGIFStruct.local_colormap, 0xFF, mColormapSize); + } else { + mColormapSize = mGIFStruct.local_colormap_buffer_size; + } + + mColormap = mGIFStruct.local_colormap; + } + + MOZ_ASSERT(mColormap); + + const size_t size = 3 << depth; + if (mColormapSize > size) { + // Clear the part of the colormap which will be unused with this palette. + // If a GIF references an invalid palette entry, ensure the entry is + // opaque white. This is needed for Skia as if it isn't, RGBX surfaces + // will cause blending issues with Skia. + memset(reinterpret_cast(mColormap) + size, 0xFF, + mColormapSize - size); + } + + MOZ_ASSERT(mColorTablePos == 0); + + // We read the local color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + return Transition::ToUnbuffered(State::FINISHED_LOCAL_COLOR_TABLE, + State::LOCAL_COLOR_TABLE, size); + } + + // There's no local color table; copy the global color table into the palette + // of the current frame. + if (mColormap) { + memcpy(mColormap, mGIFStruct.global_colormap, mColormapSize); + } else { + mColormap = mGIFStruct.global_colormap; + } + + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadLocalColorTable( + const char* aData, size_t aLength) { + uint8_t* dest = reinterpret_cast(mColormap) + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::LOCAL_COLOR_TABLE); +} + +LexerTransition nsGIFDecoder2::FinishedLocalColorTable() { + ConvertColormap(mColormap, mGIFStruct.local_colormap_size); + mColorTablePos = 0; + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadImageDataBlock( + const char* aData) { + // Make sure the transparent pixel is transparent in the colormap. + if (mGIFStruct.is_transparent) { + // Save the old value so we can restore it later. + if (mColormap == mGIFStruct.global_colormap) { + mOldColor = mColormap[mGIFStruct.tpixel]; + } + mColormap[mGIFStruct.tpixel] = 0; + } + + // Initialize the LZW decoder. + mGIFStruct.datasize = uint8_t(aData[0]); + if (mGIFStruct.datasize > MAX_LZW_BITS) { + return Transition::TerminateFailure(); + } + const int clearCode = ClearCode(); + if (clearCode >= MAX_BITS) { + return Transition::TerminateFailure(); + } + + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.datum = mGIFStruct.bits = 0; + + // Initialize the tables. + for (int i = 0; i < clearCode; i++) { + mGIFStruct.suffix[i] = i; + } + + mGIFStruct.stackp = mGIFStruct.stack; + + // Begin reading image data sub-blocks. + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition nsGIFDecoder2::ReadImageDataSubBlock( + const char* aData) { + const uint8_t subBlockLength = aData[0]; + if (subBlockLength == 0) { + // We hit the block terminator. + EndImageFrame(); + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + if (mGIFStruct.pixels_remaining == 0) { + // We've already written to the entire image; we should've hit the block + // terminator at this point. This image is corrupt, but we'll tolerate it. + + if (subBlockLength == GIF_TRAILER) { + // This GIF is missing the block terminator for the final block; we'll put + // up with it. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // We're not at the end of the image, so just skip the extra data. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, + State::SKIP_LZW_DATA, subBlockLength); + } + + // Handle the standard case: there's data in the sub-block and pixels left to + // fill in the image. We read the sub-block unbuffered so we can get pixels on + // the screen as soon as possible. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, State::LZW_DATA, + subBlockLength); +} + +LexerTransition nsGIFDecoder2::ReadLZWData( + const char* aData, size_t aLength) { + const uint8_t* data = reinterpret_cast(aData); + size_t length = aLength; + + while (mGIFStruct.pixels_remaining > 0 && + (length > 0 || mGIFStruct.bits >= mGIFStruct.codesize)) { + size_t bytesRead = 0; + + auto result = mPipe.WritePixelBlocks( + [&](uint32_t* aPixelBlock, int32_t aBlockSize) { + return YieldPixels(data, length, &bytesRead, aPixelBlock, + aBlockSize); + }); + + if (MOZ_UNLIKELY(bytesRead > length)) { + MOZ_ASSERT_UNREACHABLE("Overread?"); + bytesRead = length; + } + + // Advance our position in the input based upon what YieldPixel() consumed. + data += bytesRead; + length -= bytesRead; + + switch (result) { + case WriteState::NEED_MORE_DATA: + continue; + + case WriteState::FINISHED: + NS_WARNING_ASSERTION(mGIFStruct.pixels_remaining <= 0, + "too many pixels"); + mGIFStruct.pixels_remaining = 0; + break; + + case WriteState::FAILURE: + return Transition::TerminateFailure(); + } + } + + // We're done, but keep going until we consume all the data in the sub-block. + return Transition::ContinueUnbuffered(State::LZW_DATA); +} + +LexerTransition nsGIFDecoder2::SkipSubBlocks( + const char* aData) { + // In the SKIP_SUB_BLOCKS state we skip over data sub-blocks that we're not + // interested in. Blocks consist of a block header (which can be up to 255 + // bytes in length) and a series of data sub-blocks. Each data sub-block + // consists of a single byte length value, followed by the data itself. A data + // sub-block with a length of zero terminates the overall block. + // SKIP_SUB_BLOCKS reads a sub-block length value. If it's zero, we've arrived + // at the next block. Otherwise, we enter the SKIP_DATA_THEN_SKIP_SUB_BLOCKS + // state to skip over the sub-block data and return to SKIP_SUB_BLOCKS at the + // start of the next sub-block. + + const uint8_t nextSubBlockLength = aData[0]; + if (nextSubBlockLength == 0) { + // We hit the block terminator, so the sequence of data sub-blocks is over; + // begin processing another block. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // Skip to the next sub-block length value. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + nextSubBlockLength); +} + +Maybe nsGIFDecoder2::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_GIF); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsGIFDecoder2.h b/image/decoders/nsGIFDecoder2.h new file mode 100644 index 0000000000..5a6c501778 --- /dev/null +++ b/image/decoders/nsGIFDecoder2.h @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsGIFDecoder2_h +#define mozilla_image_decoders_nsGIFDecoder2_h + +#include "Decoder.h" +#include "GIF2.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" +#include "mozilla/gfx/Swizzle.h" + +namespace mozilla { +namespace image { +class RasterImage; + +////////////////////////////////////////////////////////////////////// +// nsGIFDecoder2 Definition + +class nsGIFDecoder2 : public Decoder { + public: + ~nsGIFDecoder2(); + + DecoderType GetType() const override { return DecoderType::GIF; } + + protected: + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + + Maybe SpeedHistogram() const override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsGIFDecoder2(RasterImage* aImage); + + /// Called when we begin decoding the image. + void BeginGIF(); + + /** + * Called when we begin decoding a frame. + * + * @param aFrameRect The region of the image that contains data. The region + * outside this rect is transparent. + * @param aDepth The palette depth of this frame. + * @param aIsInterlaced If true, this frame is an interlaced frame. + */ + nsresult BeginImageFrame(const OrientedIntRect& aFrameRect, uint16_t aDepth, + bool aIsInterlaced); + + /// Called when we finish decoding a frame. + void EndImageFrame(); + + /// Called when we finish decoding the entire image. + void FlushImageData(); + + /// Convert color map to BGRA, applying any necessary CMS transforms. + void ConvertColormap(uint32_t* aColormap, uint32_t aColors); + + /// Transforms a palette index into a pixel. + template + PixelSize ColormapIndexToPixel(uint8_t aIndex); + + /// A generator function that performs LZW decompression and yields pixels. + template + std::tuple> YieldPixels(const uint8_t* aData, + size_t aLength, + size_t* aBytesReadOut, + PixelSize* aPixelBlock, + int32_t aBlockSize); + + /// Checks if we have transparency, either because the header indicates that + /// there's alpha, or because the frame rect doesn't cover the entire image. + bool CheckForTransparency(const OrientedIntRect& aFrameRect); + + // @return the clear code used for LZW decompression. + int ClearCode() const { + MOZ_ASSERT(mGIFStruct.datasize <= MAX_LZW_BITS); + return 1 << mGIFStruct.datasize; + } + + enum class State { + FAILURE, + SUCCESS, + GIF_HEADER, + SCREEN_DESCRIPTOR, + GLOBAL_COLOR_TABLE, + FINISHED_GLOBAL_COLOR_TABLE, + BLOCK_HEADER, + EXTENSION_HEADER, + GRAPHIC_CONTROL_EXTENSION, + APPLICATION_IDENTIFIER, + NETSCAPE_EXTENSION_SUB_BLOCK, + NETSCAPE_EXTENSION_DATA, + IMAGE_DESCRIPTOR, + FINISH_IMAGE_DESCRIPTOR, + LOCAL_COLOR_TABLE, + FINISHED_LOCAL_COLOR_TABLE, + IMAGE_DATA_BLOCK, + IMAGE_DATA_SUB_BLOCK, + LZW_DATA, + SKIP_LZW_DATA, + FINISHED_LZW_DATA, + SKIP_SUB_BLOCKS, + SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + FINISHED_SKIPPING_DATA + }; + + LexerTransition ReadGIFHeader(const char* aData); + LexerTransition ReadScreenDescriptor(const char* aData); + LexerTransition ReadGlobalColorTable(const char* aData, + size_t aLength); + LexerTransition FinishedGlobalColorTable(); + LexerTransition ReadBlockHeader(const char* aData); + LexerTransition ReadExtensionHeader(const char* aData); + LexerTransition ReadGraphicControlExtension(const char* aData); + LexerTransition ReadApplicationIdentifier(const char* aData); + LexerTransition ReadNetscapeExtensionSubBlock(const char* aData); + LexerTransition ReadNetscapeExtensionData(const char* aData); + LexerTransition ReadImageDescriptor(const char* aData); + LexerTransition FinishImageDescriptor(const char* aData); + LexerTransition ReadLocalColorTable(const char* aData, size_t aLength); + LexerTransition FinishedLocalColorTable(); + LexerTransition ReadImageDataBlock(const char* aData); + LexerTransition ReadImageDataSubBlock(const char* aData); + LexerTransition ReadLZWData(const char* aData, size_t aLength); + LexerTransition SkipSubBlocks(const char* aData); + + // The StreamingLexer used to manage input. The initial size of the buffer is + // chosen as a little larger than the maximum size of any fixed-length data we + // have to read for a state. We read variable-length data in unbuffered mode + // so the buffer shouldn't have to be resized during decoding. + StreamingLexer mLexer; + + uint32_t mOldColor; // The old value of the transparent pixel + + // The frame number of the currently-decoding frame when we're in the middle + // of decoding it, and -1 otherwise. + int32_t mCurrentFrameIndex; + + // When we're reading in the global or local color table, this records our + // current position - i.e., the offset into which the next byte should be + // written. + size_t mColorTablePos; + uint32_t* mColormap; // Current colormap to be used in Cairo format + uint32_t mColormapSize; + + uint8_t mColorMask; // Apply this to the pixel to keep within colormap + bool mGIFOpen; + bool mSawTransparency; + + gif_struct mGIFStruct; + + gfx::SwizzleRowFn mSwizzleFn; /// Method to unpack color tables from RGB. + SurfacePipe mPipe; /// The SurfacePipe used to write to the output surface. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsGIFDecoder2_h diff --git a/image/decoders/nsICODecoder.cpp b/image/decoders/nsICODecoder.cpp new file mode 100644 index 0000000000..ff37355429 --- /dev/null +++ b/image/decoders/nsICODecoder.cpp @@ -0,0 +1,709 @@ +/* vim:set tw=80 expandtab softtabstop=2 ts=2 sw=2: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This is a Cross-Platform ICO Decoder, which should work everywhere, including + * Big-Endian machines like the PowerPC. */ + +#include "nsICODecoder.h" + +#include + +#include + +#include "RasterImage.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/gfx/Swizzle.h" +#include "mozilla/UniquePtrExtensions.h" + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { + +// Constants. +static const uint32_t ICOHEADERSIZE = 6; +static const uint32_t BITMAPINFOSIZE = bmp::InfoHeaderLength::WIN_ICO; + +// ---------------------------------------- +// Actual Data Processing +// ---------------------------------------- + +// Obtains the number of colors from the bits per pixel +uint16_t nsICODecoder::GetNumColors() { + uint16_t numColors = 0; + if (mBPP <= 8) { + switch (mBPP) { + case 1: + numColors = 2; + break; + case 4: + numColors = 16; + break; + case 8: + numColors = 256; + break; + default: + numColors = (uint16_t)-1; + } + } + return numColors; +} + +nsICODecoder::nsICODecoder(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::To(ICOState::HEADER, ICOHEADERSIZE), + Transition::TerminateSuccess()), + mDirEntry(nullptr), + mNumIcons(0), + mCurrIcon(0), + mBPP(0), + mMaskRowSize(0), + mCurrMaskLine(0), + mIsCursor(false), + mHasMaskAlpha(false) {} + +nsresult nsICODecoder::FinishInternal() { + // We shouldn't be called in error cases + MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); + + return GetFinalStateFromContainedDecoder(); +} + +nsresult nsICODecoder::FinishWithErrorInternal() { + // No need to assert !mInFrame here because this condition is enforced by + // mContainedDecoder. + return GetFinalStateFromContainedDecoder(); +} + +nsresult nsICODecoder::GetFinalStateFromContainedDecoder() { + if (!mContainedDecoder) { + return NS_OK; + } + + // Let the contained decoder finish up if necessary. + FlushContainedDecoder(); + + // Make our state the same as the state of the contained decoder. + mDecodeDone = mContainedDecoder->GetDecodeDone(); + mProgress |= mContainedDecoder->TakeProgress(); + mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); + mCurrentFrame = mContainedDecoder->GetCurrentFrameRef(); + + // Finalize the frame which we deferred to ensure we could modify the final + // result (e.g. to apply the BMP mask). + MOZ_ASSERT(!mContainedDecoder->GetFinalizeFrames()); + if (mCurrentFrame) { + mCurrentFrame->FinalizeSurface(); + } + + // Propagate errors. + nsresult rv = + HasError() || mContainedDecoder->HasError() ? NS_ERROR_FAILURE : NS_OK; + + MOZ_ASSERT(NS_FAILED(rv) || !mCurrentFrame || mCurrentFrame->IsFinished()); + return rv; +} + +LexerTransition nsICODecoder::ReadHeader(const char* aData) { + // If the third byte is 1, this is an icon. If 2, a cursor. + if ((aData[2] != 1) && (aData[2] != 2)) { + return Transition::TerminateFailure(); + } + mIsCursor = (aData[2] == 2); + + // The fifth and sixth bytes specify the number of resources in the file. + mNumIcons = LittleEndian::readUint16(aData + 4); + if (mNumIcons == 0) { + return Transition::TerminateSuccess(); // Nothing to do. + } + + // Downscale-during-decode can end up decoding different resources in the ICO + // file depending on the target size. Since the resources are not necessarily + // scaled versions of the same image, some may be transparent and some may not + // be. We could be precise about transparency if we decoded the metadata of + // every resource, but for now we don't and it's safest to assume that + // transparency could be present. + PostHasTransparency(); + + return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE); +} + +size_t nsICODecoder::FirstResourceOffset() const { + MOZ_ASSERT(mNumIcons > 0, + "Calling FirstResourceOffset before processing header"); + + // The first resource starts right after the directory, which starts right + // after the ICO header. + return ICOHEADERSIZE + mNumIcons * ICODIRENTRYSIZE; +} + +LexerTransition nsICODecoder::ReadDirEntry(const char* aData) { + mCurrIcon++; + + // Ensure the resource has an offset past the ICO headers. + uint32_t offset = LittleEndian::readUint32(aData + 12); + if (offset >= FirstResourceOffset()) { + // Read the directory entry. + IconDirEntryEx e; + e.mWidth = aData[0]; + e.mHeight = aData[1]; + e.mColorCount = aData[2]; + e.mReserved = aData[3]; + e.mPlanes = LittleEndian::readUint16(aData + 4); + e.mBitCount = LittleEndian::readUint16(aData + 6); + e.mBytesInRes = LittleEndian::readUint32(aData + 8); + e.mImageOffset = offset; + e.mSize = OrientedIntSize(e.mWidth, e.mHeight); + + // Only accept entries with sufficient resource data to actually contain + // some image data. + if (e.mBytesInRes > BITMAPINFOSIZE) { + if (e.mWidth == 0 || e.mHeight == 0) { + mUnsizedDirEntries.AppendElement(e); + } else { + mDirEntries.AppendElement(e); + } + } + } + + if (mCurrIcon == mNumIcons) { + if (mUnsizedDirEntries.IsEmpty()) { + return Transition::To(ICOState::FINISHED_DIR_ENTRY, 0); + } + return Transition::To(ICOState::ITERATE_UNSIZED_DIR_ENTRY, 0); + } + + return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE); +} + +LexerTransition nsICODecoder::IterateUnsizedDirEntry() { + MOZ_ASSERT(!mUnsizedDirEntries.IsEmpty()); + + if (!mDirEntry) { + // The first time we are here, there is no entry selected. We must prepare a + // new iterator for the contained decoder to advance as it wills. Cloning at + // this point ensures it will begin at the end of the dir entries. + mReturnIterator = mLexer.Clone(*mIterator, SIZE_MAX); + if (mReturnIterator.isNothing()) { + // If we cannot read further than this point, then there is no resource + // data to read. + return Transition::TerminateFailure(); + } + } else { + // We have already selected an entry which means a metadata decoder has + // finished. Verify the size is valid and if so, add to the discovered + // resources. + if (mDirEntry->mSize.width > 0 && mDirEntry->mSize.height > 0) { + mDirEntries.AppendElement(*mDirEntry); + } + + // Remove the entry from the unsized list either way. + mDirEntry = nullptr; + mUnsizedDirEntries.RemoveElementAt(0); + + // Our iterator is at an unknown point, so reset it to the point that we + // saved. + mIterator = mLexer.Clone(*mReturnIterator, SIZE_MAX); + if (mIterator.isNothing()) { + MOZ_ASSERT_UNREACHABLE("Cannot re-clone return iterator"); + return Transition::TerminateFailure(); + } + } + + // There are no more unsized entries, so we can finally decide which entry to + // select for decoding. + if (mUnsizedDirEntries.IsEmpty()) { + mReturnIterator.reset(); + return Transition::To(ICOState::FINISHED_DIR_ENTRY, 0); + } + + // Move to the resource data to start metadata decoding. + mDirEntry = &mUnsizedDirEntries[0]; + size_t offsetToResource = mDirEntry->mImageOffset - FirstResourceOffset(); + return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE, + ICOState::SKIP_TO_RESOURCE, offsetToResource); +} + +LexerTransition nsICODecoder::FinishDirEntry() { + MOZ_ASSERT(!mDirEntry); + + if (mDirEntries.IsEmpty()) { + return Transition::TerminateFailure(); + } + + // If an explicit output size was specified, we'll try to select the resource + // that matches it best below. + const Maybe desiredSize = ExplicitOutputSize(); + + // Determine the biggest resource. We always use the biggest resource for the + // intrinsic size, and if we don't have a specific desired size, we select it + // as the best resource as well. + int32_t bestDelta = INT32_MIN; + IconDirEntryEx* biggestEntry = nullptr; + + for (size_t i = 0; i < mDirEntries.Length(); ++i) { + IconDirEntryEx& e = mDirEntries[i]; + mImageMetadata.AddNativeSize(e.mSize); + + if (!biggestEntry || + (e.mBitCount >= biggestEntry->mBitCount && + e.mSize.width * e.mSize.height >= + biggestEntry->mSize.width * biggestEntry->mSize.height)) { + biggestEntry = &e; + + if (!desiredSize) { + mDirEntry = &e; + } + } + + if (desiredSize) { + // Calculate the delta between this resource's size and the desired size, + // so we can see if it is better than our current-best option. In the + // case of several equally-good resources, we use the last one. "Better" + // in this case is determined by |delta|, a measure of the difference in + // size between the entry we've found and the desired size. We will choose + // the smallest resource that is greater than or equal to the desired size + // (i.e. we assume it's better to downscale a larger icon than to upscale + // a smaller one). + int32_t delta = std::min(e.mSize.width - desiredSize->width, + e.mSize.height - desiredSize->height); + if (!mDirEntry || (e.mBitCount >= mDirEntry->mBitCount && + ((bestDelta < 0 && delta >= bestDelta) || + (delta >= 0 && delta <= bestDelta)))) { + mDirEntry = &e; + bestDelta = delta; + } + } + } + + MOZ_ASSERT(mDirEntry); + MOZ_ASSERT(biggestEntry); + + // If this is a cursor, set the hotspot. We use the hotspot from the biggest + // resource since we also use that resource for the intrinsic size. + if (mIsCursor) { + mImageMetadata.SetHotspot(biggestEntry->mXHotspot, biggestEntry->mYHotspot); + } + + // We always report the biggest resource's size as the intrinsic size; this + // is necessary for downscale-during-decode to work since we won't even + // attempt to *upscale* while decoding. + PostSize(biggestEntry->mSize.width, biggestEntry->mSize.height); + if (HasError()) { + return Transition::TerminateFailure(); + } + + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + if (mDirEntry->mSize == OutputSize()) { + // If the resource we selected matches the output size perfectly, we don't + // need to do any downscaling. + MOZ_ASSERT_IF(desiredSize, mDirEntry->mSize == *desiredSize); + MOZ_ASSERT_IF(!desiredSize, mDirEntry->mSize == Size()); + } else if (OutputSize().width < mDirEntry->mSize.width || + OutputSize().height < mDirEntry->mSize.height) { + // Create a downscaler if we need to downscale. + // + // TODO(aosmond): This is the last user of Downscaler. We should switch this + // to SurfacePipe as well so we can remove the code from tree. + mDownscaler.emplace(OutputSize().ToUnknownSize()); + } + + size_t offsetToResource = mDirEntry->mImageOffset - FirstResourceOffset(); + return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE, + ICOState::SKIP_TO_RESOURCE, offsetToResource); +} + +LexerTransition nsICODecoder::SniffResource(const char* aData) { + MOZ_ASSERT(mDirEntry); + + // We have BITMAPINFOSIZE bytes buffered at this point. We know an embedded + // BMP will have at least that many bytes by definition. We can also infer + // that any valid embedded PNG will contain that many bytes as well because: + // BITMAPINFOSIZE + // < + // signature (8 bytes) + + // IHDR (12 bytes header + 13 bytes data) + // IDAT (12 bytes header) + + // We use the first PNGSIGNATURESIZE bytes to determine whether this resource + // is a PNG or a BMP. + bool isPNG = + !memcmp(aData, nsPNGDecoder::pngSignatureBytes, PNGSIGNATURESIZE); + if (isPNG) { + if (mDirEntry->mBytesInRes <= BITMAPINFOSIZE) { + return Transition::TerminateFailure(); + } + + // Prepare a new iterator for the contained decoder to advance as it wills. + // Cloning at the point ensures it will begin at the resource offset. + Maybe containedIterator = + mLexer.Clone(*mIterator, mDirEntry->mBytesInRes); + if (containedIterator.isNothing()) { + return Transition::TerminateFailure(); + } + + // Create a PNG decoder which will do the rest of the work for us. + bool metadataDecode = mReturnIterator.isSome(); + Maybe expectedSize = + metadataDecode ? Nothing() : Some(mDirEntry->mSize); + mContainedDecoder = DecoderFactory::CreateDecoderForICOResource( + DecoderType::PNG, std::move(containedIterator.ref()), WrapNotNull(this), + metadataDecode, expectedSize); + + // Read in the rest of the PNG unbuffered. + size_t toRead = mDirEntry->mBytesInRes - BITMAPINFOSIZE; + return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, + ICOState::READ_RESOURCE, toRead); + } + + // Make sure we have a sane size for the bitmap information header. + int32_t bihSize = LittleEndian::readUint32(aData); + if (bihSize != static_cast(BITMAPINFOSIZE)) { + return Transition::TerminateFailure(); + } + + // Read in the rest of the bitmap information header. + return ReadBIH(aData); +} + +LexerTransition nsICODecoder::ReadResource() { + if (!FlushContainedDecoder()) { + return Transition::TerminateFailure(); + } + + return Transition::ContinueUnbuffered(ICOState::READ_RESOURCE); +} + +LexerTransition nsICODecoder::ReadBIH(const char* aData) { + MOZ_ASSERT(mDirEntry); + + // Extract the BPP from the BIH header; it should be trusted over the one + // we have from the ICO header which is usually set to 0. + mBPP = LittleEndian::readUint16(aData + 14); + + // Check to make sure we have valid color settings. + uint16_t numColors = GetNumColors(); + if (numColors == uint16_t(-1)) { + return Transition::TerminateFailure(); + } + + // The color table is present only if BPP is <= 8. + MOZ_ASSERT_IF(mBPP > 8, numColors == 0); + + // The ICO format when containing a BMP does not include the 14 byte + // bitmap file header. So we create the BMP decoder via the constructor that + // tells it to skip this, and pass in the required data (dataOffset) that + // would have been present in the header. + uint32_t dataOffset = + bmp::FILE_HEADER_LENGTH + BITMAPINFOSIZE + 4 * numColors; + + // Prepare a new iterator for the contained decoder to advance as it wills. + // Cloning at the point ensures it will begin at the resource offset. + Maybe containedIterator = + mLexer.Clone(*mIterator, mDirEntry->mBytesInRes); + if (containedIterator.isNothing()) { + return Transition::TerminateFailure(); + } + + // Create a BMP decoder which will do most of the work for us; the exception + // is the AND mask, which isn't present in standalone BMPs. + bool metadataDecode = mReturnIterator.isSome(); + Maybe expectedSize = + metadataDecode ? Nothing() : Some(mDirEntry->mSize); + mContainedDecoder = DecoderFactory::CreateDecoderForICOResource( + DecoderType::BMP, std::move(containedIterator.ref()), WrapNotNull(this), + metadataDecode, expectedSize, Some(dataOffset)); + + RefPtr bmpDecoder = + static_cast(mContainedDecoder.get()); + + // Ensure the decoder has parsed at least the BMP's bitmap info header. + if (!FlushContainedDecoder()) { + return Transition::TerminateFailure(); + } + + // If this is a metadata decode, FinishResource will any necessary checks. + if (mContainedDecoder->IsMetadataDecode()) { + return Transition::To(ICOState::FINISHED_RESOURCE, 0); + } + + // Do we have an AND mask on this BMP? If so, we need to read it after we read + // the BMP data itself. + uint32_t bmpDataLength = bmpDecoder->GetCompressedImageSize() + 4 * numColors; + bool hasANDMask = (BITMAPINFOSIZE + bmpDataLength) < mDirEntry->mBytesInRes; + ICOState afterBMPState = + hasANDMask ? ICOState::PREPARE_FOR_MASK : ICOState::FINISHED_RESOURCE; + + // Read in the rest of the BMP unbuffered. + return Transition::ToUnbuffered(afterBMPState, ICOState::READ_RESOURCE, + bmpDataLength); +} + +LexerTransition nsICODecoder::PrepareForMask() { + MOZ_ASSERT(mDirEntry); + MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); + + // We have received all of the data required by the BMP decoder so flushing + // here guarantees the decode has finished. + if (!FlushContainedDecoder()) { + return Transition::TerminateFailure(); + } + + MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); + + RefPtr bmpDecoder = + static_cast(mContainedDecoder.get()); + + uint16_t numColors = GetNumColors(); + MOZ_ASSERT(numColors != uint16_t(-1)); + + // Determine the length of the AND mask. + uint32_t bmpLengthWithHeader = + BITMAPINFOSIZE + bmpDecoder->GetCompressedImageSize() + 4 * numColors; + MOZ_ASSERT(bmpLengthWithHeader < mDirEntry->mBytesInRes); + uint32_t maskLength = mDirEntry->mBytesInRes - bmpLengthWithHeader; + + // If the BMP provides its own transparency, we ignore the AND mask. + if (bmpDecoder->HasTransparency()) { + return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, + ICOState::SKIP_MASK, maskLength); + } + + // Compute the row size for the mask. + mMaskRowSize = ((mDirEntry->mSize.width + 31) / 32) * 4; // + 31 to round up + + // If the expected size of the AND mask is larger than its actual size, then + // we must have a truncated (and therefore corrupt) AND mask. + uint32_t expectedLength = mMaskRowSize * mDirEntry->mSize.height; + if (maskLength < expectedLength) { + return Transition::TerminateFailure(); + } + + // If we're downscaling, the mask is the wrong size for the surface we've + // produced, so we need to downscale the mask into a temporary buffer and then + // combine the mask's alpha values with the color values from the image. + if (mDownscaler) { + MOZ_ASSERT(bmpDecoder->GetImageDataLength() == + mDownscaler->TargetSize().width * + mDownscaler->TargetSize().height * sizeof(uint32_t)); + mMaskBuffer = + MakeUniqueFallible(bmpDecoder->GetImageDataLength()); + if (NS_WARN_IF(!mMaskBuffer)) { + return Transition::TerminateFailure(); + } + nsresult rv = mDownscaler->BeginFrame(mDirEntry->mSize.ToUnknownSize(), + Nothing(), mMaskBuffer.get(), + /* aHasAlpha = */ true, + /* aFlipVertically = */ true); + if (NS_FAILED(rv)) { + return Transition::TerminateFailure(); + } + } + + mCurrMaskLine = mDirEntry->mSize.height; + return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize); +} + +LexerTransition nsICODecoder::ReadMaskRow(const char* aData) { + MOZ_ASSERT(mDirEntry); + + mCurrMaskLine--; + + uint8_t sawTransparency = 0; + + // Get the mask row we're reading. + const uint8_t* mask = reinterpret_cast(aData); + const uint8_t* maskRowEnd = mask + mMaskRowSize; + + // Get the corresponding row of the mask buffer (if we're downscaling) or the + // decoded image data (if we're not). + uint32_t* decoded = nullptr; + if (mDownscaler) { + // Initialize the row to all white and fully opaque. + memset(mDownscaler->RowBuffer(), 0xFF, + mDirEntry->mSize.width * sizeof(uint32_t)); + + decoded = reinterpret_cast(mDownscaler->RowBuffer()); + } else { + RefPtr bmpDecoder = + static_cast(mContainedDecoder.get()); + uint32_t* imageData = bmpDecoder->GetImageData(); + if (!imageData) { + return Transition::TerminateFailure(); + } + + decoded = imageData + mCurrMaskLine * mDirEntry->mSize.width; + } + + MOZ_ASSERT(decoded); + uint32_t* decodedRowEnd = decoded + mDirEntry->mSize.width; + + // Iterate simultaneously through the AND mask and the image data. + while (mask < maskRowEnd) { + uint8_t idx = *mask++; + sawTransparency |= idx; + for (uint8_t bit = 0x80; bit && decoded < decodedRowEnd; bit >>= 1) { + // Clear pixel completely for transparency. + if (idx & bit) { + *decoded = 0; + } + decoded++; + } + } + + if (mDownscaler) { + mDownscaler->CommitRow(); + } + + // If any bits are set in sawTransparency, then we know at least one pixel was + // transparent. + if (sawTransparency) { + mHasMaskAlpha = true; + } + + if (mCurrMaskLine == 0) { + return Transition::To(ICOState::FINISH_MASK, 0); + } + + return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize); +} + +LexerTransition nsICODecoder::FinishMask() { + // If we're downscaling, we now have the appropriate alpha values in + // mMaskBuffer. We just need to transfer them to the image. + if (mDownscaler) { + // Retrieve the image data. + RefPtr bmpDecoder = + static_cast(mContainedDecoder.get()); + uint8_t* imageData = reinterpret_cast(bmpDecoder->GetImageData()); + if (!imageData) { + return Transition::TerminateFailure(); + } + + // Iterate through the alpha values, copying from mask to image. + MOZ_ASSERT(mMaskBuffer); + MOZ_ASSERT(bmpDecoder->GetImageDataLength() > 0); + for (size_t i = 3; i < bmpDecoder->GetImageDataLength(); i += 4) { + imageData[i] = mMaskBuffer[i]; + } + int32_t stride = mDownscaler->TargetSize().width * sizeof(uint32_t); + DebugOnly ret = + // We know the format is OS_RGBA because we always assume bmp's inside + // ico's are transparent. + PremultiplyData(imageData, stride, SurfaceFormat::OS_RGBA, imageData, + stride, SurfaceFormat::OS_RGBA, + mDownscaler->TargetSize()); + MOZ_ASSERT(ret); + } + + return Transition::To(ICOState::FINISHED_RESOURCE, 0); +} + +LexerTransition nsICODecoder::FinishResource() { + MOZ_ASSERT(mDirEntry); + + // We have received all of the data required by the PNG/BMP decoder so + // flushing here guarantees the decode has finished. + if (!FlushContainedDecoder()) { + return Transition::TerminateFailure(); + } + + MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); + + // If it is a metadata decode, all we were trying to get was the size + // information missing from the dir entry. + if (mContainedDecoder->IsMetadataDecode()) { + if (mContainedDecoder->HasSize()) { + mDirEntry->mSize = mContainedDecoder->Size(); + } + return Transition::To(ICOState::ITERATE_UNSIZED_DIR_ENTRY, 0); + } + + // Raymond Chen says that 32bpp only are valid PNG ICOs + // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx + if (!mContainedDecoder->IsValidICOResource()) { + return Transition::TerminateFailure(); + } + + // This size from the resource should match that from the dir entry. + MOZ_ASSERT_IF(mContainedDecoder->HasSize(), + mContainedDecoder->Size() == mDirEntry->mSize); + + return Transition::TerminateSuccess(); +} + +LexerResult nsICODecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex( + aIterator, aOnResume, + [=](ICOState aState, const char* aData, size_t aLength) { + switch (aState) { + case ICOState::HEADER: + return ReadHeader(aData); + case ICOState::DIR_ENTRY: + return ReadDirEntry(aData); + case ICOState::FINISHED_DIR_ENTRY: + return FinishDirEntry(); + case ICOState::ITERATE_UNSIZED_DIR_ENTRY: + return IterateUnsizedDirEntry(); + case ICOState::SKIP_TO_RESOURCE: + return Transition::ContinueUnbuffered(ICOState::SKIP_TO_RESOURCE); + case ICOState::FOUND_RESOURCE: + return Transition::To(ICOState::SNIFF_RESOURCE, BITMAPINFOSIZE); + case ICOState::SNIFF_RESOURCE: + return SniffResource(aData); + case ICOState::READ_RESOURCE: + return ReadResource(); + case ICOState::PREPARE_FOR_MASK: + return PrepareForMask(); + case ICOState::READ_MASK_ROW: + return ReadMaskRow(aData); + case ICOState::FINISH_MASK: + return FinishMask(); + case ICOState::SKIP_MASK: + return Transition::ContinueUnbuffered(ICOState::SKIP_MASK); + case ICOState::FINISHED_RESOURCE: + return FinishResource(); + default: + MOZ_CRASH("Unknown ICOState"); + } + }); +} + +bool nsICODecoder::FlushContainedDecoder() { + MOZ_ASSERT(mContainedDecoder); + + bool succeeded = true; + + // If we run out of data, the ICO decoder will get resumed when there's more + // data available, as usual, so we don't need the contained decoder to get + // resumed too. To avoid that, we provide an IResumable which just does + // nothing. All the caller needs to do is flush when there is new data. + LexerResult result = mContainedDecoder->Decode(); + if (result == LexerResult(TerminalState::FAILURE)) { + succeeded = false; + } + + MOZ_ASSERT(result != LexerResult(Yield::OUTPUT_AVAILABLE), + "Unexpected yield"); + + // Make our state the same as the state of the contained decoder, and + // propagate errors. + mProgress |= mContainedDecoder->TakeProgress(); + mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); + if (mContainedDecoder->HasError()) { + succeeded = false; + } + + return succeeded; +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsICODecoder.h b/image/decoders/nsICODecoder.h new file mode 100644 index 0000000000..4e2665334e --- /dev/null +++ b/image/decoders/nsICODecoder.h @@ -0,0 +1,106 @@ +/* vim:set tw=80 expandtab softtabstop=4 ts=4 sw=2: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsICODecoder_h +#define mozilla_image_decoders_nsICODecoder_h + +#include "StreamingLexer.h" +#include "Decoder.h" +#include "Downscaler.h" +#include "imgFrame.h" +#include "mozilla/gfx/2D.h" +#include "nsBMPDecoder.h" +#include "nsPNGDecoder.h" +#include "ICOFileHeaders.h" + +namespace mozilla { +namespace image { + +class RasterImage; + +enum class ICOState { + HEADER, + DIR_ENTRY, + FINISHED_DIR_ENTRY, + ITERATE_UNSIZED_DIR_ENTRY, + SKIP_TO_RESOURCE, + FOUND_RESOURCE, + SNIFF_RESOURCE, + READ_RESOURCE, + PREPARE_FOR_MASK, + READ_MASK_ROW, + FINISH_MASK, + SKIP_MASK, + FINISHED_RESOURCE +}; + +class nsICODecoder : public Decoder { + public: + virtual ~nsICODecoder() {} + + /// @return The offset from the beginning of the ICO to the first resource. + size_t FirstResourceOffset() const; + + DecoderType GetType() const override { return DecoderType::ICO; } + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + nsresult FinishWithErrorInternal() override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsICODecoder(RasterImage* aImage); + + // Flushes the contained decoder to read all available data and sets the + // appropriate errors. Returns true if there are no errors. + bool FlushContainedDecoder(); + + // Gets decoder state from the contained decoder so it's visible externally. + nsresult GetFinalStateFromContainedDecoder(); + + // Obtains the number of colors from the BPP, mBPP must be filled in + uint16_t GetNumColors(); + + LexerTransition ReadHeader(const char* aData); + LexerTransition ReadDirEntry(const char* aData); + LexerTransition IterateUnsizedDirEntry(); + LexerTransition FinishDirEntry(); + LexerTransition SniffResource(const char* aData); + LexerTransition ReadResource(); + LexerTransition ReadBIH(const char* aData); + LexerTransition PrepareForMask(); + LexerTransition ReadMaskRow(const char* aData); + LexerTransition FinishMask(); + LexerTransition FinishResource(); + + struct IconDirEntryEx : public IconDirEntry { + OrientedIntSize mSize; + }; + + StreamingLexer mLexer; // The lexer. + Maybe mDownscaler; // The downscaler used for the mask. + RefPtr mContainedDecoder; // Either a BMP or PNG decoder. + Maybe + mReturnIterator; // Iterator to save return point. + UniquePtr mMaskBuffer; // A temporary buffer for the alpha mask. + nsTArray mDirEntries; // Valid dir entries with a size. + nsTArray mUnsizedDirEntries; // Dir entries without a size. + IconDirEntryEx* mDirEntry; // The dir entry for the selected resource. + uint16_t mNumIcons; // Stores the number of icons in the ICO file. + uint16_t mCurrIcon; // Stores the current dir entry index we are processing. + uint16_t mBPP; // The BPP of the resource we're decoding. + uint32_t + mMaskRowSize; // The size in bytes of each row in the BMP alpha mask. + uint32_t mCurrMaskLine; // The line of the BMP alpha mask we're processing. + bool mIsCursor; // Is this ICO a cursor? + bool mHasMaskAlpha; // Did the BMP alpha mask have any transparency? +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsICODecoder_h diff --git a/image/decoders/nsIconDecoder.cpp b/image/decoders/nsIconDecoder.cpp new file mode 100644 index 0000000000..c4ed8b7b06 --- /dev/null +++ b/image/decoders/nsIconDecoder.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIconDecoder.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" +#include "gfxPlatform.h" + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { + +static const uint32_t ICON_HEADER_SIZE = 4; + +nsIconDecoder::nsIconDecoder(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::To(State::HEADER, ICON_HEADER_SIZE), + Transition::TerminateSuccess()), + mBytesPerRow() // set by ReadHeader() +{ + // Nothing to do +} + +nsIconDecoder::~nsIconDecoder() {} + +LexerResult nsIconDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::HEADER: + return ReadHeader(aData); + case State::ROW_OF_PIXELS: + return ReadRowOfPixels(aData, aLength); + case State::FINISH: + return Finish(); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition nsIconDecoder::ReadHeader( + const char* aData) { + // Grab the width and height. + uint8_t width = uint8_t(aData[0]); + uint8_t height = uint8_t(aData[1]); + SurfaceFormat format = SurfaceFormat(aData[2]); + bool transform = bool(aData[3]); + + // FIXME(aosmond): On OSX we get the icon in device space and already + // premultiplied, so we can't support the surface flags with icons right now. + SurfacePipeFlags pipeFlags = SurfacePipeFlags(); + if (transform) { + if (mCMSMode == CMSMode::All) { + mTransform = GetCMSsRGBTransform(format); + } + + if (!(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA)) { + pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA; + } + } + + // The input is 32bpp, so we expect 4 bytes of data per pixel. + mBytesPerRow = width * 4; + + // Post our size to the superclass. + PostSize(width, height); + + // Icons have alpha. + PostHasTransparency(); + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + MOZ_ASSERT(!mImageData, "Already have a buffer allocated?"); + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), FullFrame(), format, SurfaceFormat::OS_RGBA, + /* aAnimParams */ Nothing(), mTransform, pipeFlags); + if (!pipe) { + return Transition::TerminateFailure(); + } + + mPipe = std::move(*pipe); + + MOZ_ASSERT(mImageData, "Should have a buffer now"); + + return Transition::To(State::ROW_OF_PIXELS, mBytesPerRow); +} + +LexerTransition nsIconDecoder::ReadRowOfPixels( + const char* aData, size_t aLength) { + MOZ_ASSERT(aLength % 4 == 0, "Rows should contain a multiple of four bytes"); + + auto result = mPipe.WriteBuffer(reinterpret_cast(aData)); + MOZ_ASSERT(result != WriteState::FAILURE); + + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + return result == WriteState::FINISHED + ? Transition::To(State::FINISH, 0) + : Transition::To(State::ROW_OF_PIXELS, mBytesPerRow); +} + +LexerTransition nsIconDecoder::Finish() { + PostFrameStop(); + PostDecodeDone(); + + return Transition::TerminateSuccess(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsIconDecoder.h b/image/decoders/nsIconDecoder.h new file mode 100644 index 0000000000..73bc1b5731 --- /dev/null +++ b/image/decoders/nsIconDecoder.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsIconDecoder_h +#define mozilla_image_decoders_nsIconDecoder_h + +#include "Decoder.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { + +class RasterImage; + +//////////////////////////////////////////////////////////////////////////////// +// The icon decoder is a decoder specifically tailored for loading icons +// from the OS. We've defined our own little format to represent these icons +// and this decoder takes that format and converts it into 24-bit RGB with +// alpha channel support. It was modeled a bit off the PPM decoder. +// +// The format of the incoming data is as follows: +// +// The first two bytes contain the width and the height of the icon. +// The remaining bytes contain the icon data, 4 bytes per pixel, in +// ARGB order (platform endianness, A in highest bits, B in lowest +// bits), row-primary, top-to-bottom, left-to-right, with +// premultiplied alpha. +// +//////////////////////////////////////////////////////////////////////////////// + +class nsIconDecoder : public Decoder { + public: + virtual ~nsIconDecoder(); + + DecoderType GetType() const override { return DecoderType::ICON; } + + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsIconDecoder(RasterImage* aImage); + + enum class State { HEADER, ROW_OF_PIXELS, FINISH }; + + LexerTransition ReadHeader(const char* aData); + LexerTransition ReadRowOfPixels(const char* aData, size_t aLength); + LexerTransition Finish(); + + StreamingLexer mLexer; + SurfacePipe mPipe; + uint32_t mBytesPerRow; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsIconDecoder_h diff --git a/image/decoders/nsJPEGDecoder.cpp b/image/decoders/nsJPEGDecoder.cpp new file mode 100644 index 0000000000..0a9c2cc478 --- /dev/null +++ b/image/decoders/nsJPEGDecoder.cpp @@ -0,0 +1,999 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first. + +#include "nsJPEGDecoder.h" + +#include + +#include "imgFrame.h" +#include "Orientation.h" +#include "EXIF.h" +#include "SurfacePipeFactory.h" + +#include "nspr.h" +#include "nsCRT.h" +#include "gfxColor.h" + +#include "jerror.h" + +#include "gfxPlatform.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/Telemetry.h" + +extern "C" { +#include "iccjpeg.h" +} + +#if MOZ_BIG_ENDIAN() +# define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_XRGB +#else +# define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_BGRX +#endif + +static void cmyk_convert_bgra(uint32_t* aInput, uint32_t* aOutput, + int32_t aWidth); + +using mozilla::gfx::SurfaceFormat; + +namespace mozilla { +namespace image { + +static mozilla::LazyLogModule sJPEGLog("JPEGDecoder"); + +static mozilla::LazyLogModule sJPEGDecoderAccountingLog( + "JPEGDecoderAccounting"); + +static qcms_profile* GetICCProfile(struct jpeg_decompress_struct& info) { + JOCTET* profilebuf; + uint32_t profileLength; + qcms_profile* profile = nullptr; + + if (read_icc_profile(&info, &profilebuf, &profileLength)) { + profile = qcms_profile_from_memory(profilebuf, profileLength); + free(profilebuf); + } + + return profile; +} + +METHODDEF(void) init_source(j_decompress_ptr jd); +METHODDEF(boolean) fill_input_buffer(j_decompress_ptr jd); +METHODDEF(void) skip_input_data(j_decompress_ptr jd, long num_bytes); +METHODDEF(void) term_source(j_decompress_ptr jd); +METHODDEF(void) my_error_exit(j_common_ptr cinfo); +METHODDEF(void) progress_monitor(j_common_ptr info); + +// Normal JFIF markers can't have more bytes than this. +#define MAX_JPEG_MARKER_LENGTH (((uint32_t)1 << 16) - 1) + +nsJPEGDecoder::nsJPEGDecoder(RasterImage* aImage, + Decoder::DecodeStyle aDecodeStyle) + : Decoder(aImage), + mLexer(Transition::ToUnbuffered(State::FINISHED_JPEG_DATA, + State::JPEG_DATA, SIZE_MAX), + Transition::TerminateSuccess()), + mProfile(nullptr), + mProfileLength(0), + mCMSLine(nullptr), + mDecodeStyle(aDecodeStyle) { + this->mErr.pub.error_exit = nullptr; + this->mErr.pub.emit_message = nullptr; + this->mErr.pub.output_message = nullptr; + this->mErr.pub.format_message = nullptr; + this->mErr.pub.reset_error_mgr = nullptr; + this->mErr.pub.msg_code = 0; + this->mErr.pub.trace_level = 0; + this->mErr.pub.num_warnings = 0; + this->mErr.pub.jpeg_message_table = nullptr; + this->mErr.pub.last_jpeg_message = 0; + this->mErr.pub.addon_message_table = nullptr; + this->mErr.pub.first_addon_message = 0; + this->mErr.pub.last_addon_message = 0; + mState = JPEG_HEADER; + mReading = true; + mImageData = nullptr; + + mBytesToSkip = 0; + memset(&mInfo, 0, sizeof(jpeg_decompress_struct)); + memset(&mSourceMgr, 0, sizeof(mSourceMgr)); + memset(&mProgressMgr, 0, sizeof(mProgressMgr)); + mInfo.client_data = (void*)this; + + mSegment = nullptr; + mSegmentLen = 0; + + mBackBuffer = nullptr; + mBackBufferLen = mBackBufferSize = mBackBufferUnreadLen = 0; + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::nsJPEGDecoder: Creating JPEG decoder %p", this)); +} + +nsJPEGDecoder::~nsJPEGDecoder() { + // Step 8: Release JPEG decompression object + mInfo.src = nullptr; + jpeg_destroy_decompress(&mInfo); + + free(mBackBuffer); + mBackBuffer = nullptr; + + delete[] mCMSLine; + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::~nsJPEGDecoder: Destroying JPEG decoder %p", this)); +} + +Maybe nsJPEGDecoder::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_JPEG); +} + +nsresult nsJPEGDecoder::InitInternal() { + // We set up the normal JPEG error routines, then override error_exit. + mInfo.err = jpeg_std_error(&mErr.pub); + // mInfo.err = jpeg_std_error(&mErr.pub); + mErr.pub.error_exit = my_error_exit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(mErr.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error, and initialization + // has failed. + return NS_ERROR_FAILURE; + } + + // Step 1: allocate and initialize JPEG decompression object + jpeg_create_decompress(&mInfo); + // Set the source manager + mInfo.src = &mSourceMgr; + + // Step 2: specify data source (eg, a file) + + // Setup callback functions. + mSourceMgr.init_source = init_source; + mSourceMgr.fill_input_buffer = fill_input_buffer; + mSourceMgr.skip_input_data = skip_input_data; + mSourceMgr.resync_to_restart = jpeg_resync_to_restart; + mSourceMgr.term_source = term_source; + + mInfo.mem->max_memory_to_use = static_cast( + std::min(SurfaceCache::MaximumCapacity(), LONG_MAX)); + + mProgressMgr.progress_monitor = &progress_monitor; + mInfo.progress = &mProgressMgr; + + // Record app markers for ICC data + for (uint32_t m = 0; m < 16; m++) { + jpeg_save_markers(&mInfo, JPEG_APP0 + m, 0xFFFF); + } + + return NS_OK; +} + +nsresult nsJPEGDecoder::FinishInternal() { + // If we're not in any sort of error case, force our state to JPEG_DONE. + if ((mState != JPEG_DONE && mState != JPEG_SINK_NON_JPEG_TRAILER) && + (mState != JPEG_ERROR) && !IsMetadataDecode()) { + mState = JPEG_DONE; + } + + return NS_OK; +} + +LexerResult nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::JPEG_DATA: + return ReadJPEGData(aData, aLength); + case State::FINISHED_JPEG_DATA: + return FinishedJPEGData(); + } + MOZ_CRASH("Unknown State"); + }); +} + +LexerTransition nsJPEGDecoder::ReadJPEGData( + const char* aData, size_t aLength) { + mSegment = reinterpret_cast(aData); + mSegmentLen = aLength; + + // Return here if there is a error within libjpeg. + nsresult error_code; + // This cast to nsresult makes sense because setjmp() returns whatever we + // passed to longjmp(), which was actually an nsresult. These error codes + // have been translated from libjpeg error codes, like so: + // JERR_OUT_OF_MEMORY => NS_ERROR_OUT_OF_MEMORY + // JERR_UNKNOWN_MARKER => NS_ERROR_ILLEGAL_VALUE + // JERR_SOF_UNSUPPORTED => NS_ERROR_INVALID_CONTENT_ENCODING + // => NS_ERROR_FAILURE + if ((error_code = static_cast(setjmp(mErr.setjmp_buffer))) != + NS_OK) { + bool fatal = true; + if (error_code == NS_ERROR_FAILURE) { + // Error due to corrupt data. Make sure that we don't feed any more data + // to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned NS_ERROR_FAILURE)")); + } else if (error_code == NS_ERROR_ILLEGAL_VALUE) { + // This is a recoverable error. Consume the marker and continue. + mInfo.unread_marker = 0; + fatal = false; + } else if (error_code == NS_ERROR_INVALID_CONTENT_ENCODING) { + // The content is encoding frames with a format that libjpeg can't handle. + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned NS_ERROR_INVALID_CONTENT_ENCODING)")); + // Check to see if we're in the done state, which indicates that we've + // already processed the main JPEG data. + bool inDoneState = (mState == JPEG_DONE); + // Whether we succeed or fail, we shouldn't send any more data. + mState = JPEG_SINK_NON_JPEG_TRAILER; + + // If we're in the done state, we exit successfully and attempt to + // display the content we've already received. Otherwise, we fallthrough + // and treat this as a fatal error. + if (inDoneState) { + return Transition::TerminateSuccess(); + } + } else { + // Error for another reason. (Possibly OOM.) + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned an error)")); + } + + if (fatal) { + return Transition::TerminateFailure(); + } + } + + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::Write -- processing JPEG data\n", this)); + + switch (mState) { + case JPEG_HEADER: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- entering JPEG_HEADER" + " case"); + + // Step 3: read file parameters with jpeg_read_header() + if (jpeg_read_header(&mInfo, TRUE) == JPEG_SUSPENDED) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (JPEG_SUSPENDED)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // Post our size to the superclass + EXIFData exif = ReadExifData(); + PostSize(mInfo.image_width, mInfo.image_height, exif.orientation, + exif.resolution); + if (HasError()) { + // Setting the size led to an error. + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // We're doing a full decode. + switch (mInfo.jpeg_color_space) { + case JCS_GRAYSCALE: + case JCS_RGB: + case JCS_YCbCr: + // By default, we will output directly to BGRA. If we need to apply + // special color transforms, this may change. + switch (SurfaceFormat::OS_RGBX) { + case SurfaceFormat::B8G8R8X8: + mInfo.out_color_space = JCS_EXT_BGRX; + break; + case SurfaceFormat::X8R8G8B8: + mInfo.out_color_space = JCS_EXT_XRGB; + break; + case SurfaceFormat::R8G8B8X8: + mInfo.out_color_space = JCS_EXT_RGBX; + break; + default: + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + break; + case JCS_CMYK: + case JCS_YCCK: + // libjpeg can convert from YCCK to CMYK, but not to XRGB. + mInfo.out_color_space = JCS_CMYK; + break; + default: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (unknown colorspace (3))")); + return Transition::TerminateFailure(); + } + + if (mCMSMode != CMSMode::Off) { + if ((mInProfile = GetICCProfile(mInfo)) != nullptr && + GetCMSOutputProfile()) { + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + + qcms_data_type outputType = gfxPlatform::GetCMSOSRGBAType(); + Maybe inputType; + if (profileSpace == icSigRgbData) { + // We can always color manage RGB profiles since it happens at the + // end of the pipeline. + inputType.emplace(outputType); + } else if (profileSpace == icSigGrayData && + mInfo.jpeg_color_space == JCS_GRAYSCALE) { + // We can only color manage gray profiles if the original color + // space is grayscale. This means we must downscale after color + // management since the downscaler assumes BGRA. + mInfo.out_color_space = JCS_GRAYSCALE; + inputType.emplace(QCMS_DATA_GRAY_8); + } + +#if 0 + // We don't currently support CMYK profiles. The following + // code dealt with lcms types. Add something like this + // back when we gain support for CMYK. + + // Adobe Photoshop writes YCCK/CMYK files with inverted data + if (mInfo.out_color_space == JCS_CMYK) { + type |= FLAVOR_SH(mInfo.saw_Adobe_marker ? 1 : 0); + } +#endif + + if (inputType) { + // Calculate rendering intent. + int intent = gfxPlatform::GetRenderingIntent(); + if (intent == -1) { + intent = qcms_profile_get_rendering_intent(mInProfile); + } + + // Create the color management transform. + mTransform = qcms_transform_create(mInProfile, *inputType, + GetCMSOutputProfile(), + outputType, (qcms_intent)intent); + } + } else if (mCMSMode == CMSMode::All) { + mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBX); + } + } + + // We don't want to use the pipe buffers directly because we don't want + // any reads on non-BGRA formatted data. + if (mInfo.out_color_space == JCS_GRAYSCALE || + mInfo.out_color_space == JCS_CMYK) { + mCMSLine = new (std::nothrow) uint32_t[mInfo.image_width]; + if (!mCMSLine) { + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (could allocate buffer for color conversion)")); + return Transition::TerminateFailure(); + } + } + + // Don't allocate a giant and superfluous memory buffer + // when not doing a progressive decode. + mInfo.buffered_image = + mDecodeStyle == PROGRESSIVE && jpeg_has_multiple_scans(&mInfo); + + /* Used to set up image size so arrays can be allocated */ + jpeg_calc_output_dimensions(&mInfo); + + // We handle the transform outside the pipeline if we are outputting in + // grayscale, because the pipeline wants BGRA pixels, particularly the + // downscaling filter, so we can't handle it after downscaling as would + // be optimal. + qcms_transform* pipeTransform = + mInfo.out_color_space != JCS_GRAYSCALE ? mTransform : nullptr; + + Maybe pipe = SurfacePipeFactory::CreateReorientSurfacePipe( + this, Size(), OutputSize(), SurfaceFormat::OS_RGBX, pipeTransform, + GetOrientation()); + if (!pipe) { + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (could not initialize surface pipe)")); + return Transition::TerminateFailure(); + } + + mPipe = std::move(*pipe); + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + (" JPEGDecoderAccounting: nsJPEGDecoder::" + "Write -- created image frame with %ux%u pixels", + mInfo.image_width, mInfo.image_height)); + + mState = JPEG_START_DECOMPRESS; + [[fallthrough]]; // to start decompressing. + } + + case JPEG_START_DECOMPRESS: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- entering" + " JPEG_START_DECOMPRESS case"); + // Step 4: set parameters for decompression + + // FIXME -- Should reset dct_method and dither mode + // for final pass of progressive JPEG + + mInfo.dct_method = JDCT_ISLOW; + mInfo.dither_mode = JDITHER_FS; + mInfo.do_fancy_upsampling = TRUE; + mInfo.enable_2pass_quant = FALSE; + mInfo.do_block_smoothing = TRUE; + + // Step 5: Start decompressor + if (jpeg_start_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_decompress())")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // If this is a progressive JPEG ... + mState = mInfo.buffered_image ? JPEG_DECOMPRESS_PROGRESSIVE + : JPEG_DECOMPRESS_SEQUENTIAL; + [[fallthrough]]; // to decompress sequential JPEG. + } + + case JPEG_DECOMPRESS_SEQUENTIAL: { + if (mState == JPEG_DECOMPRESS_SEQUENTIAL) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- " + "JPEG_DECOMPRESS_SEQUENTIAL case"); + + switch (OutputScanlines()) { + case WriteState::NEED_MORE_DATA: + MOZ_LOG( + sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - SEQUENTIAL)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + case WriteState::FINISHED: + NS_ASSERTION(mInfo.output_scanline == mInfo.output_height, + "We didn't process all of the data!"); + mState = JPEG_DONE; + break; + case WriteState::FAILURE: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (Error in pipeline from OutputScalines())")); + return Transition::TerminateFailure(); + } + } + [[fallthrough]]; // to decompress progressive JPEG. + } + + case JPEG_DECOMPRESS_PROGRESSIVE: { + if (mState == JPEG_DECOMPRESS_PROGRESSIVE) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- JPEG_DECOMPRESS_PROGRESSIVE case"); + auto AllComponentsSeen = [](jpeg_decompress_struct& info) { + bool all_components_seen = true; + if (info.coef_bits) { + for (int c = 0; c < info.num_components; ++c) { + bool current_component_seen = info.coef_bits[c][0] != -1; + all_components_seen &= current_component_seen; + } + } + return all_components_seen; + }; + int status; + int scan_to_display_first = 0; + bool all_components_seen; + all_components_seen = AllComponentsSeen(mInfo); + if (all_components_seen) { + scan_to_display_first = mInfo.input_scan_number; + } + + do { + status = jpeg_consume_input(&mInfo); + + if (status == JPEG_REACHED_SOS || status == JPEG_REACHED_EOI || + status == JPEG_SUSPENDED) { + // record the first scan where all components are present + all_components_seen = AllComponentsSeen(mInfo); + if (!scan_to_display_first && all_components_seen) { + scan_to_display_first = mInfo.input_scan_number; + } + } + } while ((status != JPEG_SUSPENDED) && (status != JPEG_REACHED_EOI)); + + if (!all_components_seen) { + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + // make sure we never try to access the non-exsitent scan 0 + if (!scan_to_display_first) { + scan_to_display_first = 1; + } + while (mState != JPEG_DONE) { + if (mInfo.output_scanline == 0) { + int scan = mInfo.input_scan_number; + + // if we haven't displayed anything yet (output_scan_number==0) + // and we have enough data for a complete scan, force output + // of the last full scan, but only if this last scan has seen + // DC data from all components + if ((mInfo.output_scan_number == 0) && + (scan > scan_to_display_first) && + (status != JPEG_REACHED_EOI)) { + scan--; + } + MOZ_ASSERT(scan > 0, "scan number to small!"); + if (!jpeg_start_output(&mInfo, scan)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + } + + if (mInfo.output_scanline == 0xffffff) { + mInfo.output_scanline = 0; + } + + switch (OutputScanlines()) { + case WriteState::NEED_MORE_DATA: + if (mInfo.output_scanline == 0) { + // didn't manage to read any lines - flag so we don't call + // jpeg_start_output() multiple times for the same scan + mInfo.output_scanline = 0xffffff; + } + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - " + "PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + case WriteState::FINISHED: + NS_ASSERTION(mInfo.output_scanline == mInfo.output_height, + "We didn't process all of the data!"); + + if (!jpeg_finish_output(&mInfo)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + if (jpeg_input_complete(&mInfo) && + (mInfo.input_scan_number == mInfo.output_scan_number)) { + mState = JPEG_DONE; + } else { + mInfo.output_scanline = 0; + mPipe.ResetToFirstRow(); + } + break; + case WriteState::FAILURE: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (Error in pipeline from OutputScalines())")); + return Transition::TerminateFailure(); + } + } + } + [[fallthrough]]; // to finish decompressing. + } + + case JPEG_DONE: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::ProcessData -- entering" + " JPEG_DONE case"); + + // Step 7: Finish decompression + + if (jpeg_finish_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_decompress() - DONE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // Make sure we don't feed any more data to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + + // We're done. + return Transition::TerminateSuccess(); + } + case JPEG_SINK_NON_JPEG_TRAILER: + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::ProcessData -- entering" + " JPEG_SINK_NON_JPEG_TRAILER case\n", + this)); + + MOZ_ASSERT_UNREACHABLE( + "Should stop getting data after entering state " + "JPEG_SINK_NON_JPEG_TRAILER"); + + return Transition::TerminateSuccess(); + + case JPEG_ERROR: + MOZ_ASSERT_UNREACHABLE( + "Should stop getting data after entering state " + "JPEG_ERROR"); + + return Transition::TerminateFailure(); + } + + MOZ_ASSERT_UNREACHABLE("Escaped the JPEG decoder state machine"); + return Transition::TerminateFailure(); +} // namespace image + +LexerTransition nsJPEGDecoder::FinishedJPEGData() { + // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read + // all that data something is really wrong. + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +EXIFData nsJPEGDecoder::ReadExifData() const { + jpeg_saved_marker_ptr marker; + + // Locate the APP1 marker, where EXIF data is stored, in the marker list. + for (marker = mInfo.marker_list; marker != nullptr; marker = marker->next) { + if (marker->marker == JPEG_APP0 + 1) { + break; + } + } + + // If we're at the end of the list, there's no EXIF data. + if (!marker) { + return EXIFData(); + } + + return EXIFParser::Parse(marker->data, + static_cast(marker->data_length), + gfx::IntSize(mInfo.image_width, mInfo.image_height)); +} + +void nsJPEGDecoder::NotifyDone() { + PostFrameStop(Opacity::FULLY_OPAQUE); + PostDecodeDone(); +} + +WriteState nsJPEGDecoder::OutputScanlines() { + auto result = mPipe.WritePixelBlocks( + [&](uint32_t* aPixelBlock, int32_t aBlockSize) { + JSAMPROW sampleRow = (JSAMPROW)(mCMSLine ? mCMSLine : aPixelBlock); + if (jpeg_read_scanlines(&mInfo, &sampleRow, 1) != 1) { + return std::make_tuple(/* aWritten */ 0, + Some(WriteState::NEED_MORE_DATA)); + } + + switch (mInfo.out_color_space) { + default: + // Already outputted directly to aPixelBlock as BGRA. + MOZ_ASSERT(!mCMSLine); + break; + case JCS_GRAYSCALE: + // The transform here does both color management, and converts the + // pixels from grayscale to BGRA. This is why we do it here, instead + // of using ColorManagementFilter in the SurfacePipe, because the + // other filters (e.g. DownscalingFilter) require BGRA pixels. + MOZ_ASSERT(mCMSLine); + qcms_transform_data(mTransform, mCMSLine, aPixelBlock, + mInfo.output_width); + break; + case JCS_CMYK: + // Convert from CMYK to BGRA + MOZ_ASSERT(mCMSLine); + cmyk_convert_bgra(mCMSLine, aPixelBlock, aBlockSize); + break; + } + + return std::make_tuple(aBlockSize, Maybe()); + }); + + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + return result; +} + +// Override the standard error method in the IJG JPEG decoder code. +METHODDEF(void) +my_error_exit(j_common_ptr cinfo) { + decoder_error_mgr* err = (decoder_error_mgr*)cinfo->err; + + // Convert error to a browser error code + nsresult error_code; + switch (err->pub.msg_code) { + case JERR_OUT_OF_MEMORY: + error_code = NS_ERROR_OUT_OF_MEMORY; + break; + case JERR_UNKNOWN_MARKER: + error_code = NS_ERROR_ILLEGAL_VALUE; + break; + case JERR_SOF_UNSUPPORTED: + error_code = NS_ERROR_INVALID_CONTENT_ENCODING; + break; + default: + error_code = NS_ERROR_FAILURE; + } + +#ifdef DEBUG + char buffer[JMSG_LENGTH_MAX]; + + // Create the message + (*err->pub.format_message)(cinfo, buffer); + + fprintf(stderr, "JPEG decoding error:\n%s\n", buffer); +#endif + + // Return control to the setjmp point. We pass an nsresult masquerading as + // an int, which works because the setjmp() caller casts it back. + longjmp(err->setjmp_buffer, static_cast(error_code)); +} + +static void progress_monitor(j_common_ptr info) { + int scan = ((j_decompress_ptr)info)->input_scan_number; + // Progressive images with a very large number of scans can cause the decoder + // to hang. Here we use the progress monitor to abort on a very large number + // of scans. 1000 is arbitrary, but much larger than the number of scans we + // might expect in a normal image. + if (scan >= 1000) { + my_error_exit(info); + } +} + +/******************************************************************************* + * This is the callback routine from the IJG JPEG library used to supply new + * data to the decompressor when its input buffer is exhausted. It juggles + * multiple buffers in an attempt to avoid unnecessary copying of input data. + * + * (A simpler scheme is possible: It's much easier to use only a single + * buffer; when fill_input_buffer() is called, move any unconsumed data + * (beyond the current pointer/count) down to the beginning of this buffer and + * then load new data into the remaining buffer space. This approach requires + * a little more data copying but is far easier to get right.) + * + * At any one time, the JPEG decompressor is either reading from the necko + * input buffer, which is volatile across top-level calls to the IJG library, + * or the "backtrack" buffer. The backtrack buffer contains the remaining + * unconsumed data from the necko buffer after parsing was suspended due + * to insufficient data in some previous call to the IJG library. + * + * When suspending, the decompressor will back up to a convenient restart + * point (typically the start of the current MCU). The variables + * next_input_byte & bytes_in_buffer indicate where the restart point will be + * if the current call returns FALSE. Data beyond this point must be + * rescanned after resumption, so it must be preserved in case the decompressor + * decides to backtrack. + * + * Returns: + * TRUE if additional data is available, FALSE if no data present and + * the JPEG library should therefore suspend processing of input stream + ******************************************************************************/ + +/******************************************************************************/ +/* data source manager method */ +/******************************************************************************/ + +/******************************************************************************/ +/* data source manager method + Initialize source. This is called by jpeg_read_header() before any + data is actually read. May leave + bytes_in_buffer set to 0 (in which case a fill_input_buffer() call + will occur immediately). +*/ +METHODDEF(void) +init_source(j_decompress_ptr jd) {} + +/******************************************************************************/ +/* data source manager method + Skip num_bytes worth of data. The buffer pointer and count should + be advanced over num_bytes input bytes, refilling the buffer as + needed. This is used to skip over a potentially large amount of + uninteresting data (such as an APPn marker). In some applications + it may be possible to optimize away the reading of the skipped data, + but it's not clear that being smart is worth much trouble; large + skips are uncommon. bytes_in_buffer may be zero on return. + A zero or negative skip count should be treated as a no-op. +*/ +METHODDEF(void) +skip_input_data(j_decompress_ptr jd, long num_bytes) { + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (num_bytes > (long)src->bytes_in_buffer) { + // Can't skip it all right now until we get more data from + // network stream. Set things up so that fill_input_buffer + // will skip remaining amount. + decoder->mBytesToSkip = (size_t)num_bytes - src->bytes_in_buffer; + src->next_input_byte += src->bytes_in_buffer; + src->bytes_in_buffer = 0; + + } else { + // Simple case. Just advance buffer pointer + + src->bytes_in_buffer -= (size_t)num_bytes; + src->next_input_byte += num_bytes; + } +} + +/******************************************************************************/ +/* data source manager method + This is called whenever bytes_in_buffer has reached zero and more + data is wanted. In typical applications, it should read fresh data + into the buffer (ignoring the current state of next_input_byte and + bytes_in_buffer), reset the pointer & count to the start of the + buffer, and return TRUE indicating that the buffer has been reloaded. + It is not necessary to fill the buffer entirely, only to obtain at + least one more byte. bytes_in_buffer MUST be set to a positive value + if TRUE is returned. A FALSE return should only be used when I/O + suspension is desired. +*/ +METHODDEF(boolean) +fill_input_buffer(j_decompress_ptr jd) { + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (decoder->mReading) { + const JOCTET* new_buffer = decoder->mSegment; + uint32_t new_buflen = decoder->mSegmentLen; + + if (!new_buffer || new_buflen == 0) { + return false; // suspend + } + + decoder->mSegmentLen = 0; + + if (decoder->mBytesToSkip) { + if (decoder->mBytesToSkip < new_buflen) { + // All done skipping bytes; Return what's left. + new_buffer += decoder->mBytesToSkip; + new_buflen -= decoder->mBytesToSkip; + decoder->mBytesToSkip = 0; + } else { + // Still need to skip some more data in the future + decoder->mBytesToSkip -= (size_t)new_buflen; + return false; // suspend + } + } + + decoder->mBackBufferUnreadLen = src->bytes_in_buffer; + + src->next_input_byte = new_buffer; + src->bytes_in_buffer = (size_t)new_buflen; + decoder->mReading = false; + + return true; + } + + if (src->next_input_byte != decoder->mSegment) { + // Backtrack data has been permanently consumed. + decoder->mBackBufferUnreadLen = 0; + decoder->mBackBufferLen = 0; + } + + // Save remainder of netlib buffer in backtrack buffer + const uint32_t new_backtrack_buflen = + src->bytes_in_buffer + decoder->mBackBufferLen; + + // Make sure backtrack buffer is big enough to hold new data. + if (decoder->mBackBufferSize < new_backtrack_buflen) { + // Check for malformed MARKER segment lengths, before allocating space + // for it + if (new_backtrack_buflen > MAX_JPEG_MARKER_LENGTH) { + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + + // Round up to multiple of 256 bytes. + const size_t roundup_buflen = ((new_backtrack_buflen + 255) >> 8) << 8; + JOCTET* buf = (JOCTET*)realloc(decoder->mBackBuffer, roundup_buflen); + // Check for OOM + if (!buf) { + decoder->mInfo.err->msg_code = JERR_OUT_OF_MEMORY; + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + decoder->mBackBuffer = buf; + decoder->mBackBufferSize = roundup_buflen; + } + + // Ensure we actually have a backtrack buffer. Without it, then we know that + // there is no data to copy and bytes_in_buffer is already zero. + if (decoder->mBackBuffer) { + // Copy remainder of netlib segment into backtrack buffer. + memmove(decoder->mBackBuffer + decoder->mBackBufferLen, + src->next_input_byte, src->bytes_in_buffer); + } else { + MOZ_ASSERT(src->bytes_in_buffer == 0); + MOZ_ASSERT(decoder->mBackBufferLen == 0); + MOZ_ASSERT(decoder->mBackBufferUnreadLen == 0); + } + + // Point to start of data to be rescanned. + src->next_input_byte = decoder->mBackBuffer + decoder->mBackBufferLen - + decoder->mBackBufferUnreadLen; + src->bytes_in_buffer += decoder->mBackBufferUnreadLen; + decoder->mBackBufferLen = (size_t)new_backtrack_buflen; + decoder->mReading = true; + + return false; +} + +/******************************************************************************/ +/* data source manager method */ +/* + * Terminate source --- called by jpeg_finish_decompress() after all + * data has been read to clean up JPEG source manager. NOT called by + * jpeg_abort() or jpeg_destroy(). + */ +METHODDEF(void) +term_source(j_decompress_ptr jd) { + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + // This function shouldn't be called if we ran into an error we didn't + // recover from. + MOZ_ASSERT(decoder->mState != JPEG_ERROR, + "Calling term_source on a JPEG with mState == JPEG_ERROR!"); + + // Notify using a helper method to get around protectedness issues. + decoder->NotifyDone(); +} + +} // namespace image +} // namespace mozilla + +///*************** Inverted CMYK -> RGB conversion ************************* +/// Input is (Inverted) CMYK stored as 4 bytes per pixel. +/// Output is RGB stored as 3 bytes per pixel. +/// @param aInput Points to row buffer containing the CMYK bytes for each pixel +/// in the row. +/// @param aOutput Points to row buffer to write BGRA to. +/// @param aWidth Number of pixels in the row. +static void cmyk_convert_bgra(uint32_t* aInput, uint32_t* aOutput, + int32_t aWidth) { + uint8_t* input = reinterpret_cast(aInput); + + for (int32_t i = 0; i < aWidth; ++i) { + // Source is 'Inverted CMYK', output is RGB. + // See: http://www.easyrgb.com/math.php?MATH=M12#text12 + // Or: http://www.ilkeratalay.com/colorspacesfaq.php#rgb + + // From CMYK to CMY + // C = ( C * ( 1 - K ) + K ) + // M = ( M * ( 1 - K ) + K ) + // Y = ( Y * ( 1 - K ) + K ) + + // From Inverted CMYK to CMY is thus: + // C = ( (1-iC) * (1 - (1-iK)) + (1-iK) ) => 1 - iC*iK + // Same for M and Y + + // Convert from CMY (0..1) to RGB (0..1) + // R = 1 - C => 1 - (1 - iC*iK) => iC*iK + // G = 1 - M => 1 - (1 - iM*iK) => iM*iK + // B = 1 - Y => 1 - (1 - iY*iK) => iY*iK + + // Convert from Inverted CMYK (0..255) to RGB (0..255) + const uint32_t iC = input[0]; + const uint32_t iM = input[1]; + const uint32_t iY = input[2]; + const uint32_t iK = input[3]; + + const uint8_t r = iC * iK / 255; + const uint8_t g = iM * iK / 255; + const uint8_t b = iY * iK / 255; + + *aOutput++ = (0xFF << mozilla::gfx::SurfaceFormatBit::OS_A) | + (r << mozilla::gfx::SurfaceFormatBit::OS_R) | + (g << mozilla::gfx::SurfaceFormatBit::OS_G) | + (b << mozilla::gfx::SurfaceFormatBit::OS_B); + input += 4; + } +} diff --git a/image/decoders/nsJPEGDecoder.h b/image/decoders/nsJPEGDecoder.h new file mode 100644 index 0000000000..fa010f9677 --- /dev/null +++ b/image/decoders/nsJPEGDecoder.h @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsJPEGDecoder_h +#define mozilla_image_decoders_nsJPEGDecoder_h + +#include "RasterImage.h" +#include "SurfacePipe.h" +#include "EXIF.h" + +// On Windows systems, RasterImage.h brings in 'windows.h', which defines INT32. +// But the jpeg decoder has its own definition of INT32. To avoid build issues, +// we need to undefine the version from 'windows.h'. +#undef INT32 + +#include "Decoder.h" + +extern "C" { +#include "jpeglib.h" +} + +#include + +namespace mozilla::image { + +typedef struct { + struct jpeg_error_mgr pub; // "public" fields for IJG library + jmp_buf setjmp_buffer; // For handling catastropic errors +} decoder_error_mgr; + +typedef enum { + JPEG_HEADER, // Reading JFIF headers + JPEG_START_DECOMPRESS, + JPEG_DECOMPRESS_PROGRESSIVE, // Output progressive pixels + JPEG_DECOMPRESS_SEQUENTIAL, // Output sequential pixels + JPEG_DONE, + JPEG_SINK_NON_JPEG_TRAILER, // Some image files have a + // non-JPEG trailer + JPEG_ERROR +} jstate; + +class RasterImage; +struct Orientation; + +class nsJPEGDecoder : public Decoder { + public: + virtual ~nsJPEGDecoder(); + + DecoderType GetType() const override { return DecoderType::JPEG; } + + void NotifyDone(); + + protected: + nsresult InitInternal() override; + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + + Maybe SpeedHistogram() const override; + + protected: + EXIFData ReadExifData() const; + WriteState OutputScanlines(); + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + nsJPEGDecoder(RasterImage* aImage, Decoder::DecodeStyle aDecodeStyle); + + enum class State { JPEG_DATA, FINISHED_JPEG_DATA }; + + void FinishRow(uint32_t aLastSourceRow); + LexerTransition ReadJPEGData(const char* aData, size_t aLength); + LexerTransition FinishedJPEGData(); + + StreamingLexer mLexer; + + public: + struct jpeg_decompress_struct mInfo; + struct jpeg_source_mgr mSourceMgr; + struct jpeg_progress_mgr mProgressMgr; + decoder_error_mgr mErr; + jstate mState; + + uint32_t mBytesToSkip; + + const JOCTET* mSegment; // The current segment we are decoding from + uint32_t mSegmentLen; // amount of data in mSegment + + JOCTET* mBackBuffer; + uint32_t mBackBufferLen; // Offset of end of active backtrack data + uint32_t mBackBufferSize; // size in bytes what mBackBuffer was created with + uint32_t mBackBufferUnreadLen; // amount of data currently in mBackBuffer + + JOCTET* mProfile; + uint32_t mProfileLength; + + uint32_t* mCMSLine; + + bool mReading; + + const Decoder::DecodeStyle mDecodeStyle; + + SurfacePipe mPipe; +}; + +} // namespace mozilla::image + +#endif // mozilla_image_decoders_nsJPEGDecoder_h diff --git a/image/decoders/nsJXLDecoder.cpp b/image/decoders/nsJXLDecoder.cpp new file mode 100644 index 0000000000..b3610f9075 --- /dev/null +++ b/image/decoders/nsJXLDecoder.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first +#include "gfxPlatform.h" +#include "jxl/codestream_header.h" +#include "jxl/decode_cxx.h" +#include "jxl/types.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/gfx/Point.h" +#include "nsJXLDecoder.h" + +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +using namespace mozilla::gfx; + +namespace mozilla::image { + +#define JXL_TRY(expr) \ + do { \ + JxlDecoderStatus _status = (expr); \ + if (_status != JXL_DEC_SUCCESS) { \ + return Transition::TerminateFailure(); \ + } \ + } while (0); + +#define JXL_TRY_BOOL(expr) \ + do { \ + bool succeeded = (expr); \ + if (!succeeded) { \ + return Transition::TerminateFailure(); \ + } \ + } while (0); + +static LazyLogModule sJXLLog("JXLDecoder"); + +nsJXLDecoder::nsJXLDecoder(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::ToUnbuffered(State::FINISHED_JXL_DATA, State::JXL_DATA, + SIZE_MAX), + Transition::TerminateSuccess()), + mDecoder(JxlDecoderMake(nullptr)), + mParallelRunner( + JxlThreadParallelRunnerMake(nullptr, PreferredThreadCount())) { + JxlDecoderSubscribeEvents(mDecoder.get(), + JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE); + JxlDecoderSetParallelRunner(mDecoder.get(), JxlThreadParallelRunner, + mParallelRunner.get()); + + MOZ_LOG(sJXLLog, LogLevel::Debug, + ("[this=%p] nsJXLDecoder::nsJXLDecoder", this)); +} + +nsJXLDecoder::~nsJXLDecoder() { + MOZ_LOG(sJXLLog, LogLevel::Debug, + ("[this=%p] nsJXLDecoder::~nsJXLDecoder", this)); +} + +size_t nsJXLDecoder::PreferredThreadCount() { + if (IsMetadataDecode()) { + return 0; // no additional worker thread + } + return JxlThreadParallelRunnerDefaultNumWorkerThreads(); +} + +LexerResult nsJXLDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + // return LexerResult(TerminalState::FAILURE); + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::JXL_DATA: + return ReadJXLData(aData, aLength); + case State::FINISHED_JXL_DATA: + return FinishedJXLData(); + } + MOZ_CRASH("Unknown State"); + }); +}; + +LexerTransition nsJXLDecoder::ReadJXLData( + const char* aData, size_t aLength) { + const uint8_t* input = (const uint8_t*)aData; + size_t length = aLength; + if (mBuffer.length() != 0) { + JXL_TRY_BOOL(mBuffer.append(aData, aLength)); + input = mBuffer.begin(); + length = mBuffer.length(); + } + JXL_TRY(JxlDecoderSetInput(mDecoder.get(), input, length)); + + while (true) { + JxlDecoderStatus status = JxlDecoderProcessInput(mDecoder.get()); + switch (status) { + case JXL_DEC_ERROR: + default: + return Transition::TerminateFailure(); + + case JXL_DEC_NEED_MORE_INPUT: { + size_t remaining = JxlDecoderReleaseInput(mDecoder.get()); + mBuffer.clear(); + JXL_TRY_BOOL(mBuffer.append(aData + aLength - remaining, remaining)); + return Transition::ContinueUnbuffered(State::JXL_DATA); + } + + case JXL_DEC_BASIC_INFO: { + JXL_TRY(JxlDecoderGetBasicInfo(mDecoder.get(), &mInfo)); + PostSize(mInfo.xsize, mInfo.ysize); + if (mInfo.alpha_bits > 0) { + PostHasTransparency(); + } + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + break; + } + + case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { + size_t size = 0; + JxlPixelFormat format{4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + JXL_TRY(JxlDecoderImageOutBufferSize(mDecoder.get(), &format, &size)); + + mOutBuffer.clear(); + JXL_TRY_BOOL(mOutBuffer.growBy(size)); + JXL_TRY(JxlDecoderSetImageOutBuffer(mDecoder.get(), &format, + mOutBuffer.begin(), size)); + break; + } + + case JXL_DEC_FULL_IMAGE: { + OrientedIntSize size(mInfo.xsize, mInfo.ysize); + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, size, OutputSize(), FullFrame(), SurfaceFormat::R8G8B8A8, + SurfaceFormat::OS_RGBA, Nothing(), nullptr, SurfacePipeFlags()); + for (uint8_t* rowPtr = mOutBuffer.begin(); rowPtr < mOutBuffer.end(); + rowPtr += mInfo.xsize * 4) { + pipe->WriteBuffer(reinterpret_cast(rowPtr)); + } + + if (Maybe invalidRect = pipe->TakeInvalidRect()) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + PostFrameStop(); + PostDecodeDone(); + return Transition::TerminateSuccess(); + } + } + } +} + +LexerTransition nsJXLDecoder::FinishedJXLData() { + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +} // namespace mozilla::image diff --git a/image/decoders/nsJXLDecoder.h b/image/decoders/nsJXLDecoder.h new file mode 100644 index 0000000000..6cde7456ca --- /dev/null +++ b/image/decoders/nsJXLDecoder.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsJXLDecoder_h +#define mozilla_image_decoders_nsJXLDecoder_h + +#include "Decoder.h" +#include "mp4parse.h" +#include "SurfacePipe.h" + +#include "jxl/decode_cxx.h" +#include "jxl/thread_parallel_runner_cxx.h" + +#include "mozilla/Telemetry.h" + +namespace mozilla::image { +class RasterImage; + +class nsJXLDecoder final : public Decoder { + public: + virtual ~nsJXLDecoder(); + + DecoderType GetType() const override { return DecoderType::JXL; } + + protected: + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsJXLDecoder(RasterImage* aImage); + + size_t PreferredThreadCount(); + + enum class State { JXL_DATA, FINISHED_JXL_DATA }; + + LexerTransition ReadJXLData(const char* aData, size_t aLength); + LexerTransition FinishedJXLData(); + + StreamingLexer mLexer; + JxlDecoderPtr mDecoder; + JxlThreadParallelRunnerPtr mParallelRunner; + Vector mBuffer; + Vector mOutBuffer; + JxlBasicInfo mInfo{}; +}; + +} // namespace mozilla::image + +#endif // mozilla_image_decoders_nsJXLDecoder_h diff --git a/image/decoders/nsPNGDecoder.cpp b/image/decoders/nsPNGDecoder.cpp new file mode 100644 index 0000000000..afc2762515 --- /dev/null +++ b/image/decoders/nsPNGDecoder.cpp @@ -0,0 +1,1035 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first +#include "nsPNGDecoder.h" + +#include +#include + +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "imgFrame.h" +#include "nsColor.h" +#include "nsRect.h" +#include "nspr.h" +#include "png.h" + +#include "RasterImage.h" +#include "SurfaceCache.h" +#include "SurfacePipeFactory.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla::gfx; + +using std::min; + +namespace mozilla { +namespace image { + +static LazyLogModule sPNGLog("PNGDecoder"); +static LazyLogModule sPNGDecoderAccountingLog("PNGDecoderAccounting"); + +// limit image dimensions (bug #251381, #591822, #967656, and #1283961) +#ifndef MOZ_PNG_MAX_WIDTH +# define MOZ_PNG_MAX_WIDTH 0x7fffffff // Unlimited +#endif +#ifndef MOZ_PNG_MAX_HEIGHT +# define MOZ_PNG_MAX_HEIGHT 0x7fffffff // Unlimited +#endif + +/* Controls the maximum chunk size configuration for libpng. We set this to a + * very large number, 256MB specifically. */ +static constexpr png_alloc_size_t kPngMaxChunkSize = 0x10000000; + +nsPNGDecoder::AnimFrameInfo::AnimFrameInfo() + : mDispose(DisposalMethod::KEEP), mBlend(BlendMethod::OVER), mTimeout(0) {} + +#ifdef PNG_APNG_SUPPORTED + +int32_t GetNextFrameDelay(png_structp aPNG, png_infop aInfo) { + // Delay, in seconds, is delayNum / delayDen. + png_uint_16 delayNum = png_get_next_frame_delay_num(aPNG, aInfo); + png_uint_16 delayDen = png_get_next_frame_delay_den(aPNG, aInfo); + + if (delayNum == 0) { + return 0; // SetFrameTimeout() will set to a minimum. + } + + if (delayDen == 0) { + delayDen = 100; // So says the APNG spec. + } + + // Need to cast delay_num to float to have a proper division and + // the result to int to avoid a compiler warning. + return static_cast(static_cast(delayNum) * 1000 / delayDen); +} + +nsPNGDecoder::AnimFrameInfo::AnimFrameInfo(png_structp aPNG, png_infop aInfo) + : mDispose(DisposalMethod::KEEP), mBlend(BlendMethod::OVER), mTimeout(0) { + png_byte dispose_op = png_get_next_frame_dispose_op(aPNG, aInfo); + png_byte blend_op = png_get_next_frame_blend_op(aPNG, aInfo); + + if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) { + mDispose = DisposalMethod::RESTORE_PREVIOUS; + } else if (dispose_op == PNG_DISPOSE_OP_BACKGROUND) { + mDispose = DisposalMethod::CLEAR; + } else { + mDispose = DisposalMethod::KEEP; + } + + if (blend_op == PNG_BLEND_OP_SOURCE) { + mBlend = BlendMethod::SOURCE; + } else { + mBlend = BlendMethod::OVER; + } + + mTimeout = GetNextFrameDelay(aPNG, aInfo); +} +#endif + +// First 8 bytes of a PNG file +const uint8_t nsPNGDecoder::pngSignatureBytes[] = {137, 80, 78, 71, + 13, 10, 26, 10}; + +nsPNGDecoder::nsPNGDecoder(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::ToUnbuffered(State::FINISHED_PNG_DATA, State::PNG_DATA, + SIZE_MAX), + Transition::TerminateSuccess()), + mNextTransition(Transition::ContinueUnbuffered(State::PNG_DATA)), + mLastChunkLength(0), + mPNG(nullptr), + mInfo(nullptr), + mCMSLine(nullptr), + interlacebuf(nullptr), + mFormat(SurfaceFormat::UNKNOWN), + mChannels(0), + mPass(0), + mFrameIsHidden(false), + mDisablePremultipliedAlpha(false), + mGotInfoCallback(false), + mUsePipeTransform(false), + mNumFrames(0) {} + +nsPNGDecoder::~nsPNGDecoder() { + if (mPNG) { + png_destroy_read_struct(&mPNG, mInfo ? &mInfo : nullptr, nullptr); + } + if (mCMSLine) { + free(mCMSLine); + } + if (interlacebuf) { + free(interlacebuf); + } +} + +nsPNGDecoder::TransparencyType nsPNGDecoder::GetTransparencyType( + const OrientedIntRect& aFrameRect) { + // Check if the image has a transparent color in its palette. + if (HasAlphaChannel()) { + return TransparencyType::eAlpha; + } + if (!aFrameRect.IsEqualEdges(FullFrame())) { + MOZ_ASSERT(HasAnimation()); + return TransparencyType::eFrameRect; + } + + return TransparencyType::eNone; +} + +void nsPNGDecoder::PostHasTransparencyIfNeeded( + TransparencyType aTransparencyType) { + switch (aTransparencyType) { + case TransparencyType::eNone: + return; + + case TransparencyType::eAlpha: + PostHasTransparency(); + return; + + case TransparencyType::eFrameRect: + // If the first frame of animated image doesn't draw into the whole image, + // then record that it is transparent. For subsequent frames, this doesn't + // affect transparency, because they're composited on top of all previous + // frames. + if (mNumFrames == 0) { + PostHasTransparency(); + } + return; + } +} + +// CreateFrame() is used for both simple and animated images. +nsresult nsPNGDecoder::CreateFrame(const FrameInfo& aFrameInfo) { + MOZ_ASSERT(HasSize()); + MOZ_ASSERT(!IsMetadataDecode()); + + // Check if we have transparency, and send notifications if needed. + auto transparency = GetTransparencyType(aFrameInfo.mFrameRect); + PostHasTransparencyIfNeeded(transparency); + mFormat = transparency == TransparencyType::eNone ? SurfaceFormat::OS_RGBX + : SurfaceFormat::OS_RGBA; + + // Make sure there's no animation or padding if we're downscaling. + MOZ_ASSERT_IF(Size() != OutputSize(), mNumFrames == 0); + MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); + MOZ_ASSERT_IF(Size() != OutputSize(), + transparency != TransparencyType::eFrameRect); + + Maybe animParams; +#ifdef PNG_APNG_SUPPORTED + if (!IsFirstFrameDecode() && png_get_valid(mPNG, mInfo, PNG_INFO_acTL)) { + mAnimInfo = AnimFrameInfo(mPNG, mInfo); + + if (mAnimInfo.mDispose == DisposalMethod::CLEAR) { + // We may have to display the background under this image during + // animation playback, so we regard it as transparent. + PostHasTransparency(); + } + + animParams.emplace( + AnimationParams{aFrameInfo.mFrameRect.ToUnknownRect(), + FrameTimeout::FromRawMilliseconds(mAnimInfo.mTimeout), + mNumFrames, mAnimInfo.mBlend, mAnimInfo.mDispose}); + } +#endif + + // If this image is interlaced, we can display better quality intermediate + // results to the user by post processing them with ADAM7InterpolatingFilter. + SurfacePipeFlags pipeFlags = aFrameInfo.mIsInterlaced + ? SurfacePipeFlags::ADAM7_INTERPOLATE + : SurfacePipeFlags(); + + if (mNumFrames == 0) { + // The first frame may be displayed progressively. + pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + } + + SurfaceFormat inFormat; + if (mTransform && !mUsePipeTransform) { + // QCMS will output in the correct format. + inFormat = mFormat; + } else if (transparency == TransparencyType::eAlpha) { + // We are outputting directly as RGBA, so we need to swap at this step. + inFormat = SurfaceFormat::R8G8B8A8; + } else { + // We have no alpha channel, so we need to unpack from RGB to BGRA. + inFormat = SurfaceFormat::R8G8B8; + } + + // Only apply premultiplication if the frame has true alpha. If we ever + // support downscaling animated images, we will need to premultiply for frame + // rect transparency when downscaling as well. + if (transparency == TransparencyType::eAlpha && !mDisablePremultipliedAlpha) { + pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA; + } + + qcms_transform* pipeTransform = mUsePipeTransform ? mTransform : nullptr; + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), aFrameInfo.mFrameRect, inFormat, mFormat, + animParams, pipeTransform, pipeFlags); + + if (!pipe) { + mPipe = SurfacePipe(); + return NS_ERROR_FAILURE; + } + + mPipe = std::move(*pipe); + + mFrameRect = aFrameInfo.mFrameRect; + mPass = 0; + + MOZ_LOG(sPNGDecoderAccountingLog, LogLevel::Debug, + ("PNGDecoderAccounting: nsPNGDecoder::CreateFrame -- created " + "image frame with %dx%d pixels for decoder %p", + mFrameRect.Width(), mFrameRect.Height(), this)); + + return NS_OK; +} + +// set timeout and frame disposal method for the current frame +void nsPNGDecoder::EndImageFrame() { + if (mFrameIsHidden) { + return; + } + + mNumFrames++; + + Opacity opacity = mFormat == SurfaceFormat::OS_RGBX + ? Opacity::FULLY_OPAQUE + : Opacity::SOME_TRANSPARENCY; + + PostFrameStop(opacity); +} + +nsresult nsPNGDecoder::InitInternal() { + mDisablePremultipliedAlpha = + bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA); + +#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED + static png_byte color_chunks[] = {99, 72, 82, 77, '\0', // cHRM + 105, 67, 67, 80, '\0'}; // iCCP + static png_byte unused_chunks[] = {98, 75, 71, 68, '\0', // bKGD + 101, 88, 73, 102, '\0', // eXIf + 104, 73, 83, 84, '\0', // hIST + 105, 84, 88, 116, '\0', // iTXt + 111, 70, 70, 115, '\0', // oFFs + 112, 67, 65, 76, '\0', // pCAL + 115, 67, 65, 76, '\0', // sCAL + 112, 72, 89, 115, '\0', // pHYs + 115, 66, 73, 84, '\0', // sBIT + 115, 80, 76, 84, '\0', // sPLT + 116, 69, 88, 116, '\0', // tEXt + 116, 73, 77, 69, '\0', // tIME + 122, 84, 88, 116, '\0'}; // zTXt +#endif + + // Initialize the container's source image header + // Always decode to 24 bit pixdepth + + mPNG = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, + nsPNGDecoder::error_callback, + nsPNGDecoder::warning_callback); + if (!mPNG) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mInfo = png_create_info_struct(mPNG); + if (!mInfo) { + png_destroy_read_struct(&mPNG, nullptr, nullptr); + return NS_ERROR_OUT_OF_MEMORY; + } + +#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED + // Ignore unused chunks + if (mCMSMode == CMSMode::Off || IsMetadataDecode()) { + png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2); + } + + png_set_keep_unknown_chunks(mPNG, 1, unused_chunks, + (int)sizeof(unused_chunks) / 5); +#endif + +#ifdef PNG_SET_USER_LIMITS_SUPPORTED + png_set_user_limits(mPNG, MOZ_PNG_MAX_WIDTH, MOZ_PNG_MAX_HEIGHT); + png_set_chunk_malloc_max(mPNG, kPngMaxChunkSize); +#endif + +#ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED + // Disallow palette-index checking, for speed; we would ignore the warning + // anyhow. This feature was added at libpng version 1.5.10 and is disabled + // in the embedded libpng but enabled by default in the system libpng. This + // call also disables it in the system libpng, for decoding speed. + // Bug #745202. + png_set_check_for_invalid_index(mPNG, 0); +#endif + +#ifdef PNG_SET_OPTION_SUPPORTED +# if defined(PNG_sRGB_PROFILE_CHECKS) && PNG_sRGB_PROFILE_CHECKS >= 0 + // Skip checking of sRGB ICC profiles + png_set_option(mPNG, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); +# endif + +# ifdef PNG_MAXIMUM_INFLATE_WINDOW + // Force a larger zlib inflate window as some images in the wild have + // incorrectly set metadata (specifically CMF bits) which prevent us from + // decoding them otherwise. + png_set_option(mPNG, PNG_MAXIMUM_INFLATE_WINDOW, PNG_OPTION_ON); +# endif +#endif + + // use this as libpng "progressive pointer" (retrieve in callbacks) + png_set_progressive_read_fn( + mPNG, static_cast(this), nsPNGDecoder::info_callback, + nsPNGDecoder::row_callback, nsPNGDecoder::end_callback); + + return NS_OK; +} + +LexerResult nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::PNG_DATA: + return ReadPNGData(aData, aLength); + case State::FINISHED_PNG_DATA: + return FinishedPNGData(); + } + MOZ_CRASH("Unknown State"); + }); +} + +LexerTransition nsPNGDecoder::ReadPNGData( + const char* aData, size_t aLength) { + // If we were waiting until after returning from a yield to call + // CreateFrame(), call it now. + if (mNextFrameInfo) { + if (NS_FAILED(CreateFrame(*mNextFrameInfo))) { + return Transition::TerminateFailure(); + } + + MOZ_ASSERT(mImageData, "Should have a buffer now"); + mNextFrameInfo = Nothing(); + } + + // libpng uses setjmp/longjmp for error handling. + if (setjmp(png_jmpbuf(mPNG))) { + return Transition::TerminateFailure(); + } + + // Pass the data off to libpng. + mLastChunkLength = aLength; + mNextTransition = Transition::ContinueUnbuffered(State::PNG_DATA); + png_process_data(mPNG, mInfo, + reinterpret_cast(const_cast((aData))), + aLength); + + // Make sure that we've reached a terminal state if decoding is done. + MOZ_ASSERT_IF(GetDecodeDone(), mNextTransition.NextStateIsTerminal()); + MOZ_ASSERT_IF(HasError(), mNextTransition.NextStateIsTerminal()); + + // Continue with whatever transition the callback code requested. We + // initialized this to Transition::ContinueUnbuffered(State::PNG_DATA) above, + // so by default we just continue the unbuffered read. + return mNextTransition; +} + +LexerTransition nsPNGDecoder::FinishedPNGData() { + // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read + // all that data something is really wrong. + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +// Sets up gamma pre-correction in libpng before our callback gets called. +// We need to do this if we don't end up with a CMS profile. +static void PNGDoGammaCorrection(png_structp png_ptr, png_infop info_ptr) { + double aGamma; + + if (png_get_gAMA(png_ptr, info_ptr, &aGamma)) { + if ((aGamma <= 0.0) || (aGamma > 21474.83)) { + aGamma = 0.45455; + png_set_gAMA(png_ptr, info_ptr, aGamma); + } + png_set_gamma(png_ptr, 2.2, aGamma); + } else { + png_set_gamma(png_ptr, 2.2, 0.45455); + } +} + +// Adapted from http://www.littlecms.com/pngchrm.c example code +uint32_t nsPNGDecoder::ReadColorProfile(png_structp png_ptr, png_infop info_ptr, + int color_type, bool* sRGBTag) { + // First try to see if iCCP chunk is present + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) { + png_uint_32 profileLen; + png_bytep profileData; + png_charp profileName; + int compression; + + png_get_iCCP(png_ptr, info_ptr, &profileName, &compression, &profileData, + &profileLen); + + mInProfile = qcms_profile_from_memory((char*)profileData, profileLen); + if (mInProfile) { + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + + bool mismatch = false; + if (color_type & PNG_COLOR_MASK_COLOR) { + if (profileSpace != icSigRgbData) { + mismatch = true; + } + } else { + if (profileSpace == icSigRgbData) { + png_set_gray_to_rgb(png_ptr); + } else if (profileSpace != icSigGrayData) { + mismatch = true; + } + } + + if (mismatch) { + qcms_profile_release(mInProfile); + mInProfile = nullptr; + } else { + return qcms_profile_get_rendering_intent(mInProfile); + } + } + } + + // Check sRGB chunk + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) { + *sRGBTag = true; + + int fileIntent; + png_set_gray_to_rgb(png_ptr); + png_get_sRGB(png_ptr, info_ptr, &fileIntent); + uint32_t map[] = {QCMS_INTENT_PERCEPTUAL, QCMS_INTENT_RELATIVE_COLORIMETRIC, + QCMS_INTENT_SATURATION, + QCMS_INTENT_ABSOLUTE_COLORIMETRIC}; + return map[fileIntent]; + } + + // Check gAMA/cHRM chunks + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) && + png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM)) { + qcms_CIE_xyYTRIPLE primaries; + qcms_CIE_xyY whitePoint; + + png_get_cHRM(png_ptr, info_ptr, &whitePoint.x, &whitePoint.y, + &primaries.red.x, &primaries.red.y, &primaries.green.x, + &primaries.green.y, &primaries.blue.x, &primaries.blue.y); + whitePoint.Y = primaries.red.Y = primaries.green.Y = primaries.blue.Y = 1.0; + + double gammaOfFile; + + png_get_gAMA(png_ptr, info_ptr, &gammaOfFile); + + mInProfile = qcms_profile_create_rgb_with_gamma(whitePoint, primaries, + 1.0 / gammaOfFile); + + if (mInProfile) { + png_set_gray_to_rgb(png_ptr); + } + } + + return QCMS_INTENT_PERCEPTUAL; // Our default +} + +void nsPNGDecoder::info_callback(png_structp png_ptr, png_infop info_ptr) { + png_uint_32 width, height; + int bit_depth, color_type, interlace_type, compression_type, filter_type; + unsigned int channels; + + png_bytep trans = nullptr; + int num_trans = 0; + + nsPNGDecoder* decoder = + static_cast(png_get_progressive_ptr(png_ptr)); + + if (decoder->mGotInfoCallback) { + MOZ_LOG(sPNGLog, LogLevel::Warning, + ("libpng called info_callback more than once\n")); + return; + } + + decoder->mGotInfoCallback = true; + + // Always decode to 24-bit RGB or 32-bit RGBA + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, + &interlace_type, &compression_type, &filter_type); + + const OrientedIntRect frameRect(0, 0, width, height); + + // Post our size to the superclass + decoder->PostSize(frameRect.Width(), frameRect.Height()); + + if (width > SurfaceCache::MaximumCapacity() / (bit_depth > 8 ? 16 : 8)) { + // libpng needs space to allocate two row buffers + png_error(decoder->mPNG, "Image is too wide"); + } + + if (decoder->HasError()) { + // Setting the size led to an error. + png_error(decoder->mPNG, "Sizing error"); + } + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_expand(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand(png_ptr); + } + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_color_16p trans_values; + png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values); + // libpng doesn't reject a tRNS chunk with out-of-range samples + // so we check it here to avoid setting up a useless opacity + // channel or producing unexpected transparent pixels (bug #428045) + if (bit_depth < 16) { + png_uint_16 sample_max = (1 << bit_depth) - 1; + if ((color_type == PNG_COLOR_TYPE_GRAY && + trans_values->gray > sample_max) || + (color_type == PNG_COLOR_TYPE_RGB && + (trans_values->red > sample_max || + trans_values->green > sample_max || + trans_values->blue > sample_max))) { + // clear the tRNS valid flag and release tRNS memory + png_free_data(png_ptr, info_ptr, PNG_FREE_TRNS, 0); + num_trans = 0; + } + } + if (num_trans != 0) { + png_set_expand(png_ptr); + } + } + + if (bit_depth == 16) { + png_set_scale_16(png_ptr); + } + + // We only need to extract the color profile for non-metadata decodes. It is + // fairly expensive to read the profile and create the transform so we should + // avoid it if not necessary. + uint32_t intent = -1; + bool sRGBTag = false; + if (!decoder->IsMetadataDecode()) { + if (decoder->mCMSMode != CMSMode::Off) { + intent = gfxPlatform::GetRenderingIntent(); + uint32_t pIntent = + decoder->ReadColorProfile(png_ptr, info_ptr, color_type, &sRGBTag); + // If we're not mandating an intent, use the one from the image. + if (intent == uint32_t(-1)) { + intent = pIntent; + } + } + if (!decoder->mInProfile || !decoder->GetCMSOutputProfile()) { + png_set_gray_to_rgb(png_ptr); + + // only do gamma correction if CMS isn't entirely disabled + if (decoder->mCMSMode != CMSMode::Off) { + PNGDoGammaCorrection(png_ptr, info_ptr); + } + } + } + + // Let libpng expand interlaced images. + const bool isInterlaced = interlace_type == PNG_INTERLACE_ADAM7; + if (isInterlaced) { + png_set_interlace_handling(png_ptr); + } + + // now all of those things we set above are used to update various struct + // members and whatnot, after which we can get channels, rowbytes, etc. + png_read_update_info(png_ptr, info_ptr); + decoder->mChannels = channels = png_get_channels(png_ptr, info_ptr); + + //---------------------------------------------------------------// + // copy PNG info into imagelib structs (formerly png_set_dims()) // + //---------------------------------------------------------------// + + if (channels < 1 || channels > 4) { + png_error(decoder->mPNG, "Invalid number of channels"); + } + +#ifdef PNG_APNG_SUPPORTED + bool isAnimated = png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL); + if (isAnimated) { + int32_t rawTimeout = GetNextFrameDelay(png_ptr, info_ptr); + decoder->PostIsAnimated(FrameTimeout::FromRawMilliseconds(rawTimeout)); + + if (decoder->Size() != decoder->OutputSize() && + !decoder->IsFirstFrameDecode()) { + MOZ_ASSERT_UNREACHABLE( + "Doing downscale-during-decode " + "for an animated image?"); + png_error(decoder->mPNG, "Invalid downscale attempt"); // Abort decode. + } + } +#endif + + auto transparency = decoder->GetTransparencyType(frameRect); + if (decoder->IsMetadataDecode()) { + // If we are animated then the first frame rect is either: + // 1) the whole image if the IDAT chunk is part of the animation + // 2) the frame rect of the first fDAT chunk otherwise. + // If we are not animated then we want to make sure to call + // PostHasTransparency in the metadata decode if we need to. So it's + // okay to pass IntRect(0, 0, width, height) here for animated images; + // they will call with the proper first frame rect in the full decode. + decoder->PostHasTransparencyIfNeeded(transparency); + + // We have the metadata we're looking for, so stop here, before we allocate + // buffers below. + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); + } + + if (decoder->mInProfile && decoder->GetCMSOutputProfile()) { + qcms_data_type inType; + qcms_data_type outType; + + uint32_t profileSpace = qcms_profile_get_color_space(decoder->mInProfile); + decoder->mUsePipeTransform = profileSpace != icSigGrayData; + if (decoder->mUsePipeTransform) { + // If the transform happens with SurfacePipe, it will be in RGBA if we + // have an alpha channel, because the swizzle and premultiplication + // happens after color management. Otherwise it will be in BGRA because + // the swizzle happens at the start. + if (transparency == TransparencyType::eAlpha) { + inType = QCMS_DATA_RGBA_8; + outType = QCMS_DATA_RGBA_8; + } else { + inType = gfxPlatform::GetCMSOSRGBAType(); + outType = inType; + } + } else { + if (color_type & PNG_COLOR_MASK_ALPHA) { + inType = QCMS_DATA_GRAYA_8; + outType = gfxPlatform::GetCMSOSRGBAType(); + } else { + inType = QCMS_DATA_GRAY_8; + outType = gfxPlatform::GetCMSOSRGBAType(); + } + } + + decoder->mTransform = qcms_transform_create(decoder->mInProfile, inType, + decoder->GetCMSOutputProfile(), + outType, (qcms_intent)intent); + } else if ((sRGBTag && decoder->mCMSMode == CMSMode::TaggedOnly) || + decoder->mCMSMode == CMSMode::All) { + // If the transform happens with SurfacePipe, it will be in RGBA if we + // have an alpha channel, because the swizzle and premultiplication + // happens after color management. Otherwise it will be in OS_RGBA because + // the swizzle happens at the start. + if (transparency == TransparencyType::eAlpha) { + decoder->mTransform = + decoder->GetCMSsRGBTransform(SurfaceFormat::R8G8B8A8); + } else { + decoder->mTransform = + decoder->GetCMSsRGBTransform(SurfaceFormat::OS_RGBA); + } + decoder->mUsePipeTransform = true; + } + +#ifdef PNG_APNG_SUPPORTED + if (isAnimated) { + png_set_progressive_frame_fn(png_ptr, nsPNGDecoder::frame_info_callback, + nullptr); + } + + if (png_get_first_frame_is_hidden(png_ptr, info_ptr)) { + decoder->mFrameIsHidden = true; + } else { +#endif + nsresult rv = decoder->CreateFrame(FrameInfo{frameRect, isInterlaced}); + if (NS_FAILED(rv)) { + png_error(decoder->mPNG, "CreateFrame failed"); + } + MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); +#ifdef PNG_APNG_SUPPORTED + } +#endif + + if (decoder->mTransform && !decoder->mUsePipeTransform) { + decoder->mCMSLine = + static_cast(malloc(sizeof(uint32_t) * frameRect.Width())); + if (!decoder->mCMSLine) { + png_error(decoder->mPNG, "malloc of mCMSLine failed"); + } + } + + if (interlace_type == PNG_INTERLACE_ADAM7) { + if (frameRect.Height() < + INT32_MAX / (frameRect.Width() * int32_t(channels))) { + const size_t bufferSize = + channels * frameRect.Width() * frameRect.Height(); + + if (bufferSize > SurfaceCache::MaximumCapacity()) { + png_error(decoder->mPNG, "Insufficient memory to deinterlace image"); + } + + decoder->interlacebuf = static_cast(malloc(bufferSize)); + } + if (!decoder->interlacebuf) { + png_error(decoder->mPNG, "malloc of interlacebuf failed"); + } + } +} + +void nsPNGDecoder::PostInvalidationIfNeeded() { + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (!invalidRect) { + return; + } + + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); +} + +void nsPNGDecoder::row_callback(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass) { + /* libpng comments: + * + * This function is called for every row in the image. If the + * image is interlacing, and you turned on the interlace handler, + * this function will be called for every row in every pass. + * Some of these rows will not be changed from the previous pass. + * When the row is not changed, the new_row variable will be + * nullptr. The rows and passes are called in order, so you don't + * really need the row_num and pass, but I'm supplying them + * because it may make your life easier. + * + * For the non-nullptr rows of interlaced images, you must call + * png_progressive_combine_row() passing in the row and the + * old row. You can call this function for nullptr rows (it will + * just return) and for non-interlaced images (it just does the + * memcpy for you) if it will make the code easier. Thus, you + * can just do this for all cases: + * + * png_progressive_combine_row(png_ptr, old_row, new_row); + * + * where old_row is what was displayed for previous rows. Note + * that the first pass (pass == 0 really) will completely cover + * the old row, so the rows do not have to be initialized. After + * the first pass (and only for interlaced images), you will have + * to pass the current row, and the function will combine the + * old row and the new row. + */ + nsPNGDecoder* decoder = + static_cast(png_get_progressive_ptr(png_ptr)); + + if (decoder->mFrameIsHidden) { + return; // Skip this frame. + } + + MOZ_ASSERT_IF(decoder->IsFirstFrameDecode(), decoder->mNumFrames == 0); + + while (pass > decoder->mPass) { + // Advance to the next pass. We may have to do this multiple times because + // libpng will skip passes if the image is so small that no pixels have + // changed on a given pass, but ADAM7InterpolatingFilter needs to be reset + // once for every pass to perform interpolation properly. + decoder->mPipe.ResetToFirstRow(); + decoder->mPass++; + } + + const png_uint_32 height = + static_cast(decoder->mFrameRect.Height()); + + if (row_num >= height) { + // Bail if we receive extra rows. This is especially important because if we + // didn't, we might overflow the deinterlacing buffer. + MOZ_ASSERT_UNREACHABLE("libpng producing extra rows?"); + return; + } + + // Note that |new_row| may be null here, indicating that this is an interlaced + // image and |row_callback| is being called for a row that hasn't changed. + MOZ_ASSERT_IF(!new_row, decoder->interlacebuf); + + if (decoder->interlacebuf) { + uint32_t width = uint32_t(decoder->mFrameRect.Width()); + + // We'll output the deinterlaced version of the row. + uint8_t* rowToWrite = + decoder->interlacebuf + (row_num * decoder->mChannels * width); + + // Update the deinterlaced version of this row with the new data. + png_progressive_combine_row(png_ptr, rowToWrite, new_row); + + decoder->WriteRow(rowToWrite); + } else { + decoder->WriteRow(new_row); + } +} + +void nsPNGDecoder::WriteRow(uint8_t* aRow) { + MOZ_ASSERT(aRow); + + uint8_t* rowToWrite = aRow; + uint32_t width = uint32_t(mFrameRect.Width()); + + // Apply color management to the row, if necessary, before writing it out. + // This is only needed for grayscale images. + if (mTransform && !mUsePipeTransform) { + MOZ_ASSERT(mCMSLine); + qcms_transform_data(mTransform, rowToWrite, mCMSLine, width); + rowToWrite = mCMSLine; + } + + // Write this row to the SurfacePipe. + DebugOnly result = + mPipe.WriteBuffer(reinterpret_cast(rowToWrite)); + MOZ_ASSERT(WriteState(result) != WriteState::FAILURE); + + PostInvalidationIfNeeded(); +} + +void nsPNGDecoder::DoTerminate(png_structp aPNGStruct, TerminalState aState) { + // Stop processing data. Note that we intentionally ignore the return value of + // png_process_data_pause(), which tells us how many bytes of the data that + // was passed to png_process_data() have not been consumed yet, because now + // that we've reached a terminal state, we won't do any more decoding or call + // back into libpng anymore. + png_process_data_pause(aPNGStruct, /* save = */ false); + + mNextTransition = aState == TerminalState::SUCCESS + ? Transition::TerminateSuccess() + : Transition::TerminateFailure(); +} + +void nsPNGDecoder::DoYield(png_structp aPNGStruct) { + // Pause data processing. png_process_data_pause() returns how many bytes of + // the data that was passed to png_process_data() have not been consumed yet. + // We use this information to tell StreamingLexer where to place us in the + // input stream when we come back from the yield. + png_size_t pendingBytes = png_process_data_pause(aPNGStruct, + /* save = */ false); + + MOZ_ASSERT(pendingBytes < mLastChunkLength); + size_t consumedBytes = mLastChunkLength - min(pendingBytes, mLastChunkLength); + + mNextTransition = + Transition::ContinueUnbufferedAfterYield(State::PNG_DATA, consumedBytes); +} + +nsresult nsPNGDecoder::FinishInternal() { + // We shouldn't be called in error cases. + MOZ_ASSERT(!HasError(), "Can't call FinishInternal on error!"); + + if (IsMetadataDecode()) { + return NS_OK; + } + + int32_t loop_count = 0; +#ifdef PNG_APNG_SUPPORTED + if (png_get_valid(mPNG, mInfo, PNG_INFO_acTL)) { + int32_t num_plays = png_get_num_plays(mPNG, mInfo); + loop_count = num_plays - 1; + } +#endif + + if (InFrame()) { + EndImageFrame(); + } + PostDecodeDone(loop_count); + + return NS_OK; +} + +#ifdef PNG_APNG_SUPPORTED +// got the header of a new frame that's coming +void nsPNGDecoder::frame_info_callback(png_structp png_ptr, + png_uint_32 frame_num) { + nsPNGDecoder* decoder = + static_cast(png_get_progressive_ptr(png_ptr)); + + // old frame is done + decoder->EndImageFrame(); + + const bool previousFrameWasHidden = decoder->mFrameIsHidden; + + if (!previousFrameWasHidden && decoder->IsFirstFrameDecode()) { + // We're about to get a second non-hidden frame, but we only want the first. + // Stop decoding now. (And avoid allocating the unnecessary buffers below.) + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); + } + + // Only the first frame can be hidden, so unhide unconditionally here. + decoder->mFrameIsHidden = false; + + // Save the information necessary to create the frame; we'll actually create + // it when we return from the yield. + const OrientedIntRect frameRect( + png_get_next_frame_x_offset(png_ptr, decoder->mInfo), + png_get_next_frame_y_offset(png_ptr, decoder->mInfo), + png_get_next_frame_width(png_ptr, decoder->mInfo), + png_get_next_frame_height(png_ptr, decoder->mInfo)); + const bool isInterlaced = bool(decoder->interlacebuf); + +# ifndef MOZ_EMBEDDED_LIBPNG + // if using system library, check frame_width and height against 0 + if (frameRect.width == 0) { + png_error(png_ptr, "Frame width must not be 0"); + } + if (frameRect.height == 0) { + png_error(png_ptr, "Frame height must not be 0"); + } +# endif + + const FrameInfo info{frameRect, isInterlaced}; + + // If the previous frame was hidden, skip the yield (which will mislead the + // caller, who will think the previous frame was real) and just allocate the + // new frame here. + if (previousFrameWasHidden) { + if (NS_FAILED(decoder->CreateFrame(info))) { + return decoder->DoTerminate(png_ptr, TerminalState::FAILURE); + } + + MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); + return; // No yield, so we'll just keep decoding. + } + + // Yield to the caller to notify them that the previous frame is now complete. + decoder->mNextFrameInfo = Some(info); + return decoder->DoYield(png_ptr); +} +#endif + +void nsPNGDecoder::end_callback(png_structp png_ptr, png_infop info_ptr) { + /* libpng comments: + * + * this function is called when the whole image has been read, + * including any chunks after the image (up to and including + * the IEND). You will usually have the same info chunk as you + * had in the header, although some data may have been added + * to the comments and time fields. + * + * Most people won't do much here, perhaps setting a flag that + * marks the image as finished. + */ + + nsPNGDecoder* decoder = + static_cast(png_get_progressive_ptr(png_ptr)); + + // We shouldn't get here if we've hit an error + MOZ_ASSERT(!decoder->HasError(), "Finishing up PNG but hit error!"); + + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); +} + +void nsPNGDecoder::error_callback(png_structp png_ptr, + png_const_charp error_msg) { + MOZ_LOG(sPNGLog, LogLevel::Error, ("libpng error: %s\n", error_msg)); + png_longjmp(png_ptr, 1); +} + +void nsPNGDecoder::warning_callback(png_structp png_ptr, + png_const_charp warning_msg) { + MOZ_LOG(sPNGLog, LogLevel::Warning, ("libpng warning: %s\n", warning_msg)); +} + +Maybe nsPNGDecoder::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_PNG); +} + +bool nsPNGDecoder::IsValidICOResource() const { + // Only 32-bit RGBA PNGs are valid ICO resources; see here: + // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx + + // If there are errors in the call to png_get_IHDR, the error_callback in + // nsPNGDecoder.cpp is called. In this error callback we do a longjmp, so + // we need to save the jump buffer here. Otherwise we'll end up without a + // proper callstack. + if (setjmp(png_jmpbuf(mPNG))) { + // We got here from a longjmp call indirectly from png_get_IHDR + return false; + } + + png_uint_32 png_width, // Unused + png_height; // Unused + + int png_bit_depth, png_color_type; + + if (png_get_IHDR(mPNG, mInfo, &png_width, &png_height, &png_bit_depth, + &png_color_type, nullptr, nullptr, nullptr)) { + return ((png_color_type == PNG_COLOR_TYPE_RGB_ALPHA || + png_color_type == PNG_COLOR_TYPE_RGB) && + png_bit_depth == 8); + } else { + return false; + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsPNGDecoder.h b/image/decoders/nsPNGDecoder.h new file mode 100644 index 0000000000..89d66fa5eb --- /dev/null +++ b/image/decoders/nsPNGDecoder.h @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsPNGDecoder_h +#define mozilla_image_decoders_nsPNGDecoder_h + +#include "Decoder.h" +#include "png.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" +#include "mozilla/gfx/Swizzle.h" + +namespace mozilla { +namespace image { +class RasterImage; + +class nsPNGDecoder : public Decoder { + public: + virtual ~nsPNGDecoder(); + + /// @return true if this PNG is a valid ICO resource. + bool IsValidICOResource() const override; + + DecoderType GetType() const override { return DecoderType::PNG; } + + protected: + nsresult InitInternal() override; + nsresult FinishInternal() override; + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + + Maybe SpeedHistogram() const override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsPNGDecoder(RasterImage* aImage); + + /// The information necessary to create a frame. + struct FrameInfo { + OrientedIntRect mFrameRect; + bool mIsInterlaced; + }; + + nsresult CreateFrame(const FrameInfo& aFrameInfo); + void EndImageFrame(); + + uint32_t ReadColorProfile(png_structp png_ptr, png_infop info_ptr, + int color_type, bool* sRGBTag); + + bool HasAlphaChannel() const { return mChannels == 2 || mChannels == 4; } + + enum class TransparencyType { eNone, eAlpha, eFrameRect }; + + TransparencyType GetTransparencyType(const OrientedIntRect& aFrameRect); + void PostHasTransparencyIfNeeded(TransparencyType aTransparencyType); + + void PostInvalidationIfNeeded(); + + void WriteRow(uint8_t* aRow); + + // Convenience methods to make interacting with StreamingLexer from inside + // a libpng callback easier. + void DoTerminate(png_structp aPNGStruct, TerminalState aState); + void DoYield(png_structp aPNGStruct); + + enum class State { PNG_DATA, FINISHED_PNG_DATA }; + + LexerTransition ReadPNGData(const char* aData, size_t aLength); + LexerTransition FinishedPNGData(); + + StreamingLexer mLexer; + + // The next lexer state transition. We need to store it here because we can't + // directly return arbitrary values from libpng callbacks. + LexerTransition mNextTransition; + + // We yield to the caller every time we finish decoding a frame. When this + // happens, we need to allocate the next frame after returning from the yield. + // |mNextFrameInfo| is used to store the information needed to allocate the + // next frame. + Maybe mNextFrameInfo; + + // The length of the last chunk of data passed to ReadPNGData(). We use this + // to arrange to arrive back at the correct spot in the data after yielding. + size_t mLastChunkLength; + + public: + png_structp mPNG; + png_infop mInfo; + OrientedIntRect mFrameRect; + uint8_t* mCMSLine; + uint8_t* interlacebuf; + gfx::SurfaceFormat mFormat; + + uint8_t mChannels; + uint8_t mPass; + bool mFrameIsHidden; + bool mDisablePremultipliedAlpha; + bool mGotInfoCallback; + bool mUsePipeTransform; + + struct AnimFrameInfo { + AnimFrameInfo(); +#ifdef PNG_APNG_SUPPORTED + AnimFrameInfo(png_structp aPNG, png_infop aInfo); +#endif + + DisposalMethod mDispose; + BlendMethod mBlend; + int32_t mTimeout; + }; + + AnimFrameInfo mAnimInfo; + + SurfacePipe mPipe; /// The SurfacePipe used to write to the output surface. + + // The number of frames we've finished. + uint32_t mNumFrames; + + // libpng callbacks + // We put these in the class so that they can access protected members. + static void PNGAPI info_callback(png_structp png_ptr, png_infop info_ptr); + static void PNGAPI row_callback(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass); +#ifdef PNG_APNG_SUPPORTED + static void PNGAPI frame_info_callback(png_structp png_ptr, + png_uint_32 frame_num); +#endif + static void PNGAPI end_callback(png_structp png_ptr, png_infop info_ptr); + static void PNGAPI error_callback(png_structp png_ptr, + png_const_charp error_msg); + static void PNGAPI warning_callback(png_structp png_ptr, + png_const_charp warning_msg); + + // This is defined in the PNG spec as an invariant. We use it to + // do manual validation without libpng. + static const uint8_t pngSignatureBytes[]; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsPNGDecoder_h diff --git a/image/decoders/nsWebPDecoder.cpp b/image/decoders/nsWebPDecoder.cpp new file mode 100644 index 0000000000..e7467f0066 --- /dev/null +++ b/image/decoders/nsWebPDecoder.cpp @@ -0,0 +1,605 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first +#include "gfxPlatform.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "nsWebPDecoder.h" + +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { + +static LazyLogModule sWebPLog("WebPDecoder"); + +nsWebPDecoder::nsWebPDecoder(RasterImage* aImage) + : Decoder(aImage), + mDecoder(nullptr), + mBlend(BlendMethod::OVER), + mDisposal(DisposalMethod::KEEP), + mTimeout(FrameTimeout::Forever()), + mFormat(SurfaceFormat::OS_RGBX), + mLastRow(0), + mCurrentFrame(0), + mData(nullptr), + mLength(0), + mIteratorComplete(false), + mNeedDemuxer(true), + mGotColorProfile(false) { + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::nsWebPDecoder", this)); +} + +nsWebPDecoder::~nsWebPDecoder() { + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::~nsWebPDecoder", this)); + if (mDecoder) { + WebPIDelete(mDecoder); + WebPFreeDecBuffer(&mBuffer); + } +} + +LexerResult nsWebPDecoder::ReadData() { + MOZ_ASSERT(mData); + MOZ_ASSERT(mLength > 0); + + WebPDemuxer* demuxer = nullptr; + bool complete = mIteratorComplete; + + if (mNeedDemuxer) { + WebPDemuxState state; + WebPData fragment; + fragment.bytes = mData; + fragment.size = mLength; + + demuxer = WebPDemuxPartial(&fragment, &state); + if (state == WEBP_DEMUX_PARSE_ERROR) { + MOZ_LOG( + sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ReadData -- demux parse error\n", this)); + WebPDemuxDelete(demuxer); + return LexerResult(TerminalState::FAILURE); + } + + if (state == WEBP_DEMUX_PARSING_HEADER) { + WebPDemuxDelete(demuxer); + return LexerResult(Yield::NEED_MORE_DATA); + } + + if (!demuxer) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ReadData -- no demuxer\n", this)); + return LexerResult(TerminalState::FAILURE); + } + + complete = complete || state == WEBP_DEMUX_DONE; + } + + LexerResult rv(TerminalState::FAILURE); + if (!HasSize()) { + rv = ReadHeader(demuxer, complete); + } else { + rv = ReadPayload(demuxer, complete); + } + + WebPDemuxDelete(demuxer); + return rv; +} + +LexerResult nsWebPDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + while (true) { + SourceBufferIterator::State state = SourceBufferIterator::COMPLETE; + if (!mIteratorComplete) { + state = aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume); + + // We need to remember since we can't advance a complete iterator. + mIteratorComplete = state == SourceBufferIterator::COMPLETE; + } + + if (state == SourceBufferIterator::WAITING) { + return LexerResult(Yield::NEED_MORE_DATA); + } + + LexerResult rv = UpdateBuffer(aIterator, state); + if (rv.is() && rv.as() == Yield::NEED_MORE_DATA) { + // We need to check the iterator to see if more is available before + // giving up unless we are already complete. + if (mIteratorComplete) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::DoDecode -- read all data, " + "but needs more\n", + this)); + return LexerResult(TerminalState::FAILURE); + } + continue; + } + + return rv; + } +} + +LexerResult nsWebPDecoder::UpdateBuffer(SourceBufferIterator& aIterator, + SourceBufferIterator::State aState) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + switch (aState) { + case SourceBufferIterator::READY: + if (!aIterator.IsContiguous()) { + // We need to buffer. This should be rare, but expensive. + break; + } + if (!mData) { + // For as long as we hold onto an iterator, we know the data pointers + // to the chunks cannot change underneath us, so save the pointer to + // the first block. + MOZ_ASSERT(mLength == 0); + mData = reinterpret_cast(aIterator.Data()); + } + mLength += aIterator.Length(); + return ReadData(); + case SourceBufferIterator::COMPLETE: + if (!mData) { + // We must have hit an error, such as an OOM, when buffering the + // first set of encoded data. + MOZ_LOG( + sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::DoDecode -- complete no data\n", this)); + return LexerResult(TerminalState::FAILURE); + } + return ReadData(); + default: + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::DoDecode -- bad state\n", this)); + return LexerResult(TerminalState::FAILURE); + } + + // We need to buffer. If we have no data buffered, we need to get everything + // from the first chunk of the source buffer before appending the new data. + if (mBufferedData.empty()) { + MOZ_ASSERT(mData); + MOZ_ASSERT(mLength > 0); + + if (!mBufferedData.append(mData, mLength)) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::DoDecode -- oom, initialize %zu\n", + this, mLength)); + return LexerResult(TerminalState::FAILURE); + } + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu bytes\n", this, + mLength)); + } + + // Append the incremental data from the iterator. + if (!mBufferedData.append(aIterator.Data(), aIterator.Length())) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::DoDecode -- oom, append %zu on %zu\n", + this, aIterator.Length(), mBufferedData.length())); + return LexerResult(TerminalState::FAILURE); + } + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu -> %zu bytes\n", + this, aIterator.Length(), mBufferedData.length())); + mData = mBufferedData.begin(); + mLength = mBufferedData.length(); + return ReadData(); +} + +nsresult nsWebPDecoder::CreateFrame(const OrientedIntRect& aFrameRect) { + MOZ_ASSERT(HasSize()); + MOZ_ASSERT(!mDecoder); + + MOZ_LOG( + sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::CreateFrame -- frame %u, (%d, %d) %d x %d\n", + this, mCurrentFrame, aFrameRect.x, aFrameRect.y, aFrameRect.width, + aFrameRect.height)); + + if (aFrameRect.width <= 0 || aFrameRect.height <= 0) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::CreateFrame -- bad frame rect\n", this)); + return NS_ERROR_FAILURE; + } + + // If this is our first frame in an animation and it doesn't cover the + // full frame, then we are transparent even if there is no alpha + if (mCurrentFrame == 0 && !aFrameRect.IsEqualEdges(FullFrame())) { + MOZ_ASSERT(HasAnimation()); + mFormat = SurfaceFormat::OS_RGBA; + PostHasTransparency(); + } + + WebPInitDecBuffer(&mBuffer); + + switch (SurfaceFormat::OS_RGBA) { + case SurfaceFormat::B8G8R8A8: + mBuffer.colorspace = MODE_BGRA; + break; + case SurfaceFormat::A8R8G8B8: + mBuffer.colorspace = MODE_ARGB; + break; + case SurfaceFormat::R8G8B8A8: + mBuffer.colorspace = MODE_RGBA; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown OS_RGBA"); + return NS_ERROR_FAILURE; + } + + mDecoder = WebPINewDecoder(&mBuffer); + if (!mDecoder) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::CreateFrame -- create decoder error\n", + this)); + return NS_ERROR_FAILURE; + } + + // WebP doesn't guarantee that the alpha generated matches the hint in the + // header, so we always need to claim the input is BGRA. If the output is + // BGRX, swizzling will mask off the alpha channel. + SurfaceFormat inFormat = SurfaceFormat::OS_RGBA; + + SurfacePipeFlags pipeFlags = SurfacePipeFlags(); + if (mFormat == SurfaceFormat::OS_RGBA && + !(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA)) { + pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA; + } + + Maybe animParams; + if (!IsFirstFrameDecode()) { + animParams.emplace(aFrameRect.ToUnknownRect(), mTimeout, mCurrentFrame, + mBlend, mDisposal); + } + + Maybe pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), aFrameRect, inFormat, mFormat, animParams, + mTransform, pipeFlags); + if (!pipe) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::CreateFrame -- no pipe\n", this)); + return NS_ERROR_FAILURE; + } + + mFrameRect = aFrameRect; + mPipe = std::move(*pipe); + return NS_OK; +} + +void nsWebPDecoder::EndFrame() { + MOZ_ASSERT(HasSize()); + MOZ_ASSERT(mDecoder); + + auto opacity = mFormat == SurfaceFormat::OS_RGBA ? Opacity::SOME_TRANSPARENCY + : Opacity::FULLY_OPAQUE; + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::EndFrame -- frame %u, opacity %d, " + "disposal %d, timeout %d, blend %d\n", + this, mCurrentFrame, (int)opacity, (int)mDisposal, + mTimeout.AsEncodedValueDeprecated(), (int)mBlend)); + + PostFrameStop(opacity); + WebPIDelete(mDecoder); + WebPFreeDecBuffer(&mBuffer); + mDecoder = nullptr; + mLastRow = 0; + ++mCurrentFrame; +} + +void nsWebPDecoder::ApplyColorProfile(const char* aProfile, size_t aLength) { + MOZ_ASSERT(!mGotColorProfile); + mGotColorProfile = true; + + if (mCMSMode == CMSMode::Off || !GetCMSOutputProfile() || + (mCMSMode == CMSMode::TaggedOnly && !aProfile)) { + return; + } + + if (!aProfile) { + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ApplyColorProfile -- not tagged, use " + "sRGB transform\n", + this)); + mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBA); + return; + } + + mInProfile = qcms_profile_from_memory(aProfile, aLength); + if (!mInProfile) { + MOZ_LOG( + sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ApplyColorProfile -- bad color profile\n", + this)); + return; + } + + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + if (profileSpace != icSigRgbData) { + // WebP doesn't produce grayscale data, this must be corrupt. + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ApplyColorProfile -- ignoring non-rgb " + "color profile\n", + this)); + return; + } + + // Calculate rendering intent. + int intent = gfxPlatform::GetRenderingIntent(); + if (intent == -1) { + intent = qcms_profile_get_rendering_intent(mInProfile); + } + + // Create the color management transform. + qcms_data_type type = gfxPlatform::GetCMSOSRGBAType(); + mTransform = qcms_transform_create(mInProfile, type, GetCMSOutputProfile(), + type, (qcms_intent)intent); + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ApplyColorProfile -- use tagged " + "transform\n", + this)); +} + +LexerResult nsWebPDecoder::ReadHeader(WebPDemuxer* aDemuxer, bool aIsComplete) { + MOZ_ASSERT(aDemuxer); + + MOZ_LOG( + sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadHeader -- %zu bytes\n", this, mLength)); + + uint32_t flags = WebPDemuxGetI(aDemuxer, WEBP_FF_FORMAT_FLAGS); + + if (!IsMetadataDecode() && !mGotColorProfile) { + if (flags & WebPFeatureFlags::ICCP_FLAG) { + WebPChunkIterator iter; + if (WebPDemuxGetChunk(aDemuxer, "ICCP", 1, &iter)) { + ApplyColorProfile(reinterpret_cast(iter.chunk.bytes), + iter.chunk.size); + WebPDemuxReleaseChunkIterator(&iter); + + } else { + if (!aIsComplete) { + return LexerResult(Yield::NEED_MORE_DATA); + } + + MOZ_LOG(sWebPLog, LogLevel::Warning, + ("[this=%p] nsWebPDecoder::ReadHeader header specified ICCP " + "but no ICCP chunk found, ignoring\n", + this)); + + ApplyColorProfile(nullptr, 0); + } + } else { + ApplyColorProfile(nullptr, 0); + } + } + + if (flags & WebPFeatureFlags::ANIMATION_FLAG) { + // A metadata decode expects to get the correct first frame timeout which + // sadly is not provided by the normal WebP header parsing. + WebPIterator iter; + if (!WebPDemuxGetFrame(aDemuxer, 1, &iter)) { + return aIsComplete ? LexerResult(TerminalState::FAILURE) + : LexerResult(Yield::NEED_MORE_DATA); + } + + PostIsAnimated(FrameTimeout::FromRawMilliseconds(iter.duration)); + WebPDemuxReleaseIterator(&iter); + } else { + // Single frames don't need a demuxer to be created. + mNeedDemuxer = false; + } + + uint32_t width = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_WIDTH); + uint32_t height = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_HEIGHT); + if (width > INT32_MAX || height > INT32_MAX) { + return LexerResult(TerminalState::FAILURE); + } + + PostSize(width, height); + + bool alpha = flags & WebPFeatureFlags::ALPHA_FLAG; + if (alpha) { + mFormat = SurfaceFormat::OS_RGBA; + PostHasTransparency(); + } + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadHeader -- %u x %u, alpha %d, " + "animation %d, metadata decode %d, first frame decode %d\n", + this, width, height, alpha, HasAnimation(), IsMetadataDecode(), + IsFirstFrameDecode())); + + if (IsMetadataDecode()) { + return LexerResult(TerminalState::SUCCESS); + } + + return ReadPayload(aDemuxer, aIsComplete); +} + +LexerResult nsWebPDecoder::ReadPayload(WebPDemuxer* aDemuxer, + bool aIsComplete) { + if (!HasAnimation()) { + auto rv = ReadSingle(mData, mLength, FullFrame()); + if (rv.is() && + rv.as() == TerminalState::SUCCESS) { + PostDecodeDone(); + } + return rv; + } + return ReadMultiple(aDemuxer, aIsComplete); +} + +LexerResult nsWebPDecoder::ReadSingle(const uint8_t* aData, size_t aLength, + const OrientedIntRect& aFrameRect) { + MOZ_ASSERT(!IsMetadataDecode()); + MOZ_ASSERT(aData); + MOZ_ASSERT(aLength > 0); + + MOZ_LOG( + sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadSingle -- %zu bytes\n", this, aLength)); + + if (!mDecoder && NS_FAILED(CreateFrame(aFrameRect))) { + return LexerResult(TerminalState::FAILURE); + } + + bool complete; + do { + VP8StatusCode status = WebPIUpdate(mDecoder, aData, aLength); + switch (status) { + case VP8_STATUS_OK: + complete = true; + break; + case VP8_STATUS_SUSPENDED: + complete = false; + break; + default: + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ReadSingle -- append error %d\n", + this, status)); + return LexerResult(TerminalState::FAILURE); + } + + int lastRow = -1; + int width = 0; + int height = 0; + int stride = 0; + uint8_t* rowStart = + WebPIDecGetRGB(mDecoder, &lastRow, &width, &height, &stride); + + MOZ_LOG( + sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadSingle -- complete %d, read %d rows, " + "has %d rows available\n", + this, complete, mLastRow, lastRow)); + + if (!rowStart || lastRow == -1 || lastRow == mLastRow) { + return LexerResult(Yield::NEED_MORE_DATA); + } + + if (width != mFrameRect.width || height != mFrameRect.height || + stride < mFrameRect.width * 4 || lastRow > mFrameRect.height) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ReadSingle -- bad (w,h,s) = (%d, %d, " + "%d)\n", + this, width, height, stride)); + return LexerResult(TerminalState::FAILURE); + } + + for (int row = mLastRow; row < lastRow; row++) { + uint32_t* src = reinterpret_cast(rowStart + row * stride); + WriteState result = mPipe.WriteBuffer(src); + + Maybe invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + if (result == WriteState::FAILURE) { + MOZ_LOG(sWebPLog, LogLevel::Error, + ("[this=%p] nsWebPDecoder::ReadSingle -- write pixels error\n", + this)); + return LexerResult(TerminalState::FAILURE); + } + + if (result == WriteState::FINISHED) { + MOZ_ASSERT(row == lastRow - 1, "There was more data to read?"); + complete = true; + break; + } + } + + mLastRow = lastRow; + } while (!complete); + + if (!complete) { + return LexerResult(Yield::NEED_MORE_DATA); + } + + EndFrame(); + return LexerResult(TerminalState::SUCCESS); +} + +LexerResult nsWebPDecoder::ReadMultiple(WebPDemuxer* aDemuxer, + bool aIsComplete) { + MOZ_ASSERT(!IsMetadataDecode()); + MOZ_ASSERT(aDemuxer); + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadMultiple\n", this)); + + bool complete = aIsComplete; + WebPIterator iter; + auto rv = LexerResult(Yield::NEED_MORE_DATA); + if (WebPDemuxGetFrame(aDemuxer, mCurrentFrame + 1, &iter)) { + switch (iter.blend_method) { + case WEBP_MUX_BLEND: + mBlend = BlendMethod::OVER; + break; + case WEBP_MUX_NO_BLEND: + mBlend = BlendMethod::SOURCE; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled blend method"); + break; + } + + switch (iter.dispose_method) { + case WEBP_MUX_DISPOSE_NONE: + mDisposal = DisposalMethod::KEEP; + break; + case WEBP_MUX_DISPOSE_BACKGROUND: + mDisposal = DisposalMethod::CLEAR; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled dispose method"); + break; + } + + mFormat = iter.has_alpha || mCurrentFrame > 0 ? SurfaceFormat::OS_RGBA + : SurfaceFormat::OS_RGBX; + mTimeout = FrameTimeout::FromRawMilliseconds(iter.duration); + OrientedIntRect frameRect(iter.x_offset, iter.y_offset, iter.width, + iter.height); + + rv = ReadSingle(iter.fragment.bytes, iter.fragment.size, frameRect); + complete = complete && !WebPDemuxNextFrame(&iter); + WebPDemuxReleaseIterator(&iter); + } + + if (rv.is() && + rv.as() == TerminalState::SUCCESS) { + // If we extracted one frame, and it is not the last, we need to yield to + // the lexer to allow the upper layers to acknowledge the frame. + if (!complete && !IsFirstFrameDecode()) { + rv = LexerResult(Yield::OUTPUT_AVAILABLE); + } else { + uint32_t loopCount = WebPDemuxGetI(aDemuxer, WEBP_FF_LOOP_COUNT); + + MOZ_LOG(sWebPLog, LogLevel::Debug, + ("[this=%p] nsWebPDecoder::ReadMultiple -- loop count %u\n", this, + loopCount)); + PostDecodeDone(loopCount - 1); + } + } + + return rv; +} + +Maybe nsWebPDecoder::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_WEBP); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsWebPDecoder.h b/image/decoders/nsWebPDecoder.h new file mode 100644 index 0000000000..e69122d19a --- /dev/null +++ b/image/decoders/nsWebPDecoder.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_decoders_nsWebPDecoder_h +#define mozilla_image_decoders_nsWebPDecoder_h + +#include "Decoder.h" +#include "webp/demux.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { +class RasterImage; + +class nsWebPDecoder final : public Decoder { + public: + virtual ~nsWebPDecoder(); + + DecoderType GetType() const override { return DecoderType::WEBP; } + + protected: + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + Maybe SpeedHistogram() const override; + + private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsWebPDecoder(RasterImage* aImage); + + void ApplyColorProfile(const char* aProfile, size_t aLength); + + LexerResult UpdateBuffer(SourceBufferIterator& aIterator, + SourceBufferIterator::State aState); + LexerResult ReadData(); + LexerResult ReadHeader(WebPDemuxer* aDemuxer, bool aIsComplete); + LexerResult ReadPayload(WebPDemuxer* aDemuxer, bool aIsComplete); + + nsresult CreateFrame(const OrientedIntRect& aFrameRect); + void EndFrame(); + + LexerResult ReadSingle(const uint8_t* aData, size_t aLength, + const OrientedIntRect& aFrameRect); + + LexerResult ReadMultiple(WebPDemuxer* aDemuxer, bool aIsComplete); + + /// The SurfacePipe used to write to the output surface. + SurfacePipe mPipe; + + /// The buffer used to accumulate data until the complete WebP header is + /// received, if and only if the iterator is discontiguous. + Vector mBufferedData; + + /// The libwebp output buffer descriptor pointing to the decoded data. + WebPDecBuffer mBuffer; + + /// The libwebp incremental decoder descriptor, wraps mBuffer. + WebPIDecoder* mDecoder; + + /// Blend method for the current frame. + BlendMethod mBlend; + + /// Disposal method for the current frame. + DisposalMethod mDisposal; + + /// Frame timeout for the current frame; + FrameTimeout mTimeout; + + /// Surface format for the current frame. + gfx::SurfaceFormat mFormat; + + /// Frame rect for the current frame. + OrientedIntRect mFrameRect; + + /// The last row of decoded pixels written to mPipe. + int mLastRow; + + /// Number of decoded frames. + uint32_t mCurrentFrame; + + /// Pointer to the start of the contiguous encoded image data. + const uint8_t* mData; + + /// Length of data pointed to by mData. + size_t mLength; + + /// True if the iterator has reached its end. + bool mIteratorComplete; + + /// True if this decoding pass requires a WebPDemuxer. + bool mNeedDemuxer; + + /// True if we have setup the color profile for the image. + bool mGotColorProfile; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsWebPDecoder_h diff --git a/image/encoders/bmp/moz.build b/image/encoders/bmp/moz.build new file mode 100644 index 0000000000..783fd37005 --- /dev/null +++ b/image/encoders/bmp/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsBMPEncoder.cpp", +] + +LOCAL_INCLUDES += [ + "/image", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/bmp/nsBMPEncoder.cpp b/image/encoders/bmp/nsBMPEncoder.cpp new file mode 100644 index 0000000000..697950d7af --- /dev/null +++ b/image/encoders/bmp/nsBMPEncoder.cpp @@ -0,0 +1,716 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCRT.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/UniquePtrExtensions.h" +#include "nsBMPEncoder.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "nsTArray.h" +#include "mozilla/CheckedInt.h" +#include "BMPHeaders.h" + +using namespace mozilla; +using namespace mozilla::image; +using namespace mozilla::image::bmp; + +NS_IMPL_ISUPPORTS(nsBMPEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +nsBMPEncoder::nsBMPEncoder() + : mBMPInfoHeader{}, + mImageBufferStart(nullptr), + mImageBufferCurr(0), + mImageBufferSize(0), + mImageBufferReadPoint(0), + mFinished(false), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0) { + this->mBMPFileHeader.filesize = 0; + this->mBMPFileHeader.reserved = 0; + this->mBMPFileHeader.dataoffset = 0; +} + +nsBMPEncoder::~nsBMPEncoder() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferCurr = nullptr; + } +} + +// nsBMPEncoder::InitFromData +// +// One output option is supported: bpp= +// bpp specifies the bits per pixel to use where bpp_value can be 24 or 32 +NS_IMETHODIMP +nsBMPEncoder::InitFromData(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + CheckedInt32 check = CheckedInt32(aWidth) * 4; + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_INVALID_ARG; + } + + // Stride is the padded width of each row, so it better be longer + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData"); + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + if (NS_FAILED(rv)) { + return rv; + } + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + if (NS_FAILED(rv)) { + return rv; + } + + rv = EndImageEncode(); + return rv; +} + +// Just a helper method to make it explicit in calculations that we are dealing +// with bytes and not bits +static inline uint16_t BytesPerPixel(uint16_t aBPP) { return aBPP / 8; } + +// Calculates the number of padding bytes that are needed per row of image data +static inline uint32_t PaddingBytes(uint16_t aBPP, uint32_t aWidth) { + uint32_t rowSize = aWidth * BytesPerPixel(aBPP); + uint8_t paddingSize = 0; + if (rowSize % 4) { + paddingSize = (4 - (rowSize % 4)); + } + return paddingSize; +} + +// See ::InitFromData for other info. +NS_IMETHODIMP +nsBMPEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // can't initialize more than once + if (mImageBufferStart || mImageBufferCurr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + // parse and check any provided output options + Version version; + uint16_t bpp; + nsresult rv = ParseOptions(aOutputOptions, version, bpp); + if (NS_FAILED(rv)) { + return rv; + } + MOZ_ASSERT(bpp <= 32); + + rv = InitFileHeader(version, bpp, aWidth, aHeight); + if (NS_FAILED(rv)) { + return rv; + } + rv = InitInfoHeader(version, bpp, aWidth, aHeight); + if (NS_FAILED(rv)) { + return rv; + } + + mImageBufferSize = mBMPFileHeader.filesize; + mImageBufferStart = static_cast(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + + EncodeFileHeader(); + EncodeInfoHeader(); + + return NS_OK; +} + +// Returns the number of bytes in the image buffer used. +// For a BMP file, this is all bytes in the buffer. +NS_IMETHODIMP +nsBMPEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferSize; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsBMPEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast(mImageBufferStart); + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::AddImageFrame(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aFrameOptions) { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + if (mBMPInfoHeader.width < 0) { + return NS_ERROR_ILLEGAL_VALUE; + } + + CheckedUint32 size = CheckedUint32(mBMPInfoHeader.width) * + CheckedUint32(BytesPerPixel(mBMPInfoHeader.bpp)); + if (MOZ_UNLIKELY(!size.isValid())) { + return NS_ERROR_FAILURE; + } + + auto row = MakeUniqueFallible(size.value()); + if (!row) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CheckedUint32 check = CheckedUint32(mBMPInfoHeader.height) * aStride; + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_FAILURE; + } + + // write each row: if we add more input formats, we may want to + // generalize the conversions + if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + // BMP requires RGBA with post-multiplied alpha, so we need to convert + for (int32_t y = mBMPInfoHeader.height - 1; y >= 0; y--) { + ConvertHostARGBRow(&aData[y * aStride], row, mBMPInfoHeader.width); + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(row.get()); + } else { + EncodeImageDataRow32(row.get()); + } + } + } else if (aInputFormat == INPUT_FORMAT_RGBA) { + // simple RGBA, no conversion needed + for (int32_t y = 0; y < mBMPInfoHeader.height; y++) { + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(row.get()); + } else { + EncodeImageDataRow32(row.get()); + } + } + } else if (aInputFormat == INPUT_FORMAT_RGB) { + // simple RGB, no conversion needed + for (int32_t y = 0; y < mBMPInfoHeader.height; y++) { + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(&aData[y * aStride]); + } else { + EncodeImageDataRow32(&aData[y * aStride]); + } + } + } else { + MOZ_ASSERT_UNREACHABLE("Bad format type"); + return NS_ERROR_INVALID_ARG; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::EndImageEncode() { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +// Parses the encoder options and sets the bits per pixel to use +// See InitFromData for a description of the parse options +nsresult nsBMPEncoder::ParseOptions(const nsAString& aOptions, + Version& aVersionOut, uint16_t& aBppOut) { + aVersionOut = VERSION_3; + aBppOut = 24; + + // Parse the input string into a set of name/value pairs. + // From a format like: name=value;bpp=;name=value + // to format: [0] = name=value, [1] = bpp=, [2] = name=value + nsTArray nameValuePairs; + ParseString(NS_ConvertUTF16toUTF8(aOptions), ';', nameValuePairs); + + // For each name/value pair in the set + for (uint32_t i = 0; i < nameValuePairs.Length(); ++i) { + // Split the name value pair [0] = name, [1] = value + nsTArray nameValuePair; + ParseString(nameValuePairs[i], '=', nameValuePair); + if (nameValuePair.Length() != 2) { + return NS_ERROR_INVALID_ARG; + } + + // Parse the bpp portion of the string name=value;version=; + // name=value + if (nameValuePair[0].Equals("version", + nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("3")) { + aVersionOut = VERSION_3; + } else if (nameValuePair[1].EqualsLiteral("5")) { + aVersionOut = VERSION_5; + } else { + return NS_ERROR_INVALID_ARG; + } + } + + // Parse the bpp portion of the string name=value;bpp=;name=value + if (nameValuePair[0].Equals("bpp", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("24")) { + aBppOut = 24; + } else if (nameValuePair[1].EqualsLiteral("32")) { + aBppOut = 32; + } else { + return NS_ERROR_INVALID_ARG; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::Close() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferSize = 0; + mImageBufferReadPoint = 0; + mImageBufferCurr = nullptr; + } + + return NS_OK; +} + +// Obtains the available bytes to read +NS_IMETHODIMP +nsBMPEncoder::Available(uint64_t* _retval) { + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + return NS_OK; +} + +// Obtains the stream's status +NS_IMETHODIMP +nsBMPEncoder::StreamStatus() { + return mImageBufferStart && mImageBufferCurr ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +// [noscript] Reads bytes which are available +NS_IMETHODIMP +nsBMPEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +// [noscript] Reads segments +NS_IMETHODIMP +nsBMPEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + uint32_t maxCount = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast(mImageBufferStart + mImageBufferReadPoint), + 0, aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // We don't want to notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the + // main thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsBMPEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but we need +// an output with no alpha in machine-independent byte order. +// +void nsBMPEncoder::ConvertHostARGBRow(const uint8_t* aSrc, + const UniquePtr& aDest, + uint32_t aPixelWidth) { + uint16_t bytes = BytesPerPixel(mBMPInfoHeader.bpp); + + if (mBMPInfoHeader.bpp == 32) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * bytes]; + + pixelOut[0] = (pixelIn & 0x00ff0000) >> 16; + pixelOut[1] = (pixelIn & 0x0000ff00) >> 8; + pixelOut[2] = (pixelIn & 0x000000ff) >> 0; + pixelOut[3] = (pixelIn & 0xff000000) >> 24; + } + } else { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * bytes]; + + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff) >> 0; + } + } +} + +void nsBMPEncoder::NotifyListener() { + if (mCallback && (GetCurrentImageBufferOffset() - mImageBufferReadPoint >= + mNotifyThreshold || + mFinished)) { + nsCOMPtr callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsBMPEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could + // reenter AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} + +// Initializes the BMP file header mBMPFileHeader to the passed in values +nsresult nsBMPEncoder::InitFileHeader(Version aVersion, uint16_t aBPP, + uint32_t aWidth, uint32_t aHeight) { + memset(&mBMPFileHeader, 0, sizeof(mBMPFileHeader)); + mBMPFileHeader.signature[0] = 'B'; + mBMPFileHeader.signature[1] = 'M'; + + if (aVersion == VERSION_3) { + mBMPFileHeader.dataoffset = FILE_HEADER_LENGTH + InfoHeaderLength::WIN_V3; + } else { // aVersion == 5 + mBMPFileHeader.dataoffset = FILE_HEADER_LENGTH + InfoHeaderLength::WIN_V5; + } + + // The color table is present only if BPP is <= 8 + if (aBPP <= 8) { + uint32_t numColors = 1 << aBPP; + mBMPFileHeader.dataoffset += 4 * numColors; + CheckedUint32 filesize = CheckedUint32(mBMPFileHeader.dataoffset) + + CheckedUint32(aWidth) * aHeight; + if (MOZ_UNLIKELY(!filesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPFileHeader.filesize = filesize.value(); + } else { + CheckedUint32 filesize = CheckedUint32(mBMPFileHeader.dataoffset) + + (CheckedUint32(aWidth) * BytesPerPixel(aBPP) + + PaddingBytes(aBPP, aWidth)) * + aHeight; + if (MOZ_UNLIKELY(!filesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPFileHeader.filesize = filesize.value(); + } + + mBMPFileHeader.reserved = 0; + + return NS_OK; +} + +#define ENCODE(pImageBufferCurr, value) \ + memcpy(*pImageBufferCurr, &value, sizeof value); \ + *pImageBufferCurr += sizeof value; + +// Initializes the bitmap info header mBMPInfoHeader to the passed in values +nsresult nsBMPEncoder::InitInfoHeader(Version aVersion, uint16_t aBPP, + uint32_t aWidth, uint32_t aHeight) { + memset(&mBMPInfoHeader, 0, sizeof(mBMPInfoHeader)); + if (aVersion == VERSION_3) { + mBMPInfoHeader.bihsize = InfoHeaderLength::WIN_V3; + } else { + MOZ_ASSERT(aVersion == VERSION_5); + mBMPInfoHeader.bihsize = InfoHeaderLength::WIN_V5; + } + + CheckedInt32 width(aWidth); + CheckedInt32 height(aHeight); + if (MOZ_UNLIKELY(!width.isValid() || !height.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.width = width.value(); + mBMPInfoHeader.height = height.value(); + + mBMPInfoHeader.planes = 1; + mBMPInfoHeader.bpp = aBPP; + mBMPInfoHeader.compression = 0; + mBMPInfoHeader.colors = 0; + mBMPInfoHeader.important_colors = 0; + + CheckedUint32 check = CheckedUint32(aWidth) * BytesPerPixel(aBPP); + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_INVALID_ARG; + } + + if (aBPP <= 8) { + CheckedUint32 imagesize = CheckedUint32(aWidth) * aHeight; + if (MOZ_UNLIKELY(!imagesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.image_size = imagesize.value(); + } else { + CheckedUint32 imagesize = (CheckedUint32(aWidth) * BytesPerPixel(aBPP) + + PaddingBytes(aBPP, aWidth)) * + CheckedUint32(aHeight); + if (MOZ_UNLIKELY(!imagesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.image_size = imagesize.value(); + } + mBMPInfoHeader.xppm = 0; + mBMPInfoHeader.yppm = 0; + if (aVersion >= VERSION_5) { + mBMPInfoHeader.red_mask = 0x000000FF; + mBMPInfoHeader.green_mask = 0x0000FF00; + mBMPInfoHeader.blue_mask = 0x00FF0000; + mBMPInfoHeader.alpha_mask = 0xFF000000; + mBMPInfoHeader.color_space = V5InfoHeader::COLOR_SPACE_LCS_SRGB; + mBMPInfoHeader.white_point.r.x = 0; + mBMPInfoHeader.white_point.r.y = 0; + mBMPInfoHeader.white_point.r.z = 0; + mBMPInfoHeader.white_point.g.x = 0; + mBMPInfoHeader.white_point.g.y = 0; + mBMPInfoHeader.white_point.g.z = 0; + mBMPInfoHeader.white_point.b.x = 0; + mBMPInfoHeader.white_point.b.y = 0; + mBMPInfoHeader.white_point.b.z = 0; + mBMPInfoHeader.gamma_red = 0; + mBMPInfoHeader.gamma_green = 0; + mBMPInfoHeader.gamma_blue = 0; + mBMPInfoHeader.intent = 0; + mBMPInfoHeader.profile_offset = 0; + mBMPInfoHeader.profile_size = 0; + mBMPInfoHeader.reserved = 0; + } + + return NS_OK; +} + +// Encodes the BMP file header mBMPFileHeader +void nsBMPEncoder::EncodeFileHeader() { + FileHeader littleEndianBFH = mBMPFileHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.filesize, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.reserved, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.dataoffset, 1); + + ENCODE(&mImageBufferCurr, littleEndianBFH.signature); + ENCODE(&mImageBufferCurr, littleEndianBFH.filesize); + ENCODE(&mImageBufferCurr, littleEndianBFH.reserved); + ENCODE(&mImageBufferCurr, littleEndianBFH.dataoffset); +} + +// Encodes the BMP info header mBMPInfoHeader +void nsBMPEncoder::EncodeInfoHeader() { + V5InfoHeader littleEndianmBIH = mBMPInfoHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.bihsize, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.width, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.height, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.planes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.bpp, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.compression, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.image_size, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.xppm, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.yppm, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.colors, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.important_colors, + 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.red_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.green_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.blue_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.alpha_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.color_space, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_red, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_green, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_blue, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.intent, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.profile_offset, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.profile_size, 1); + + ENCODE(&mImageBufferCurr, littleEndianmBIH.bihsize); + ENCODE(&mImageBufferCurr, littleEndianmBIH.width); + ENCODE(&mImageBufferCurr, littleEndianmBIH.height); + ENCODE(&mImageBufferCurr, littleEndianmBIH.planes); + ENCODE(&mImageBufferCurr, littleEndianmBIH.bpp); + ENCODE(&mImageBufferCurr, littleEndianmBIH.compression); + ENCODE(&mImageBufferCurr, littleEndianmBIH.image_size); + ENCODE(&mImageBufferCurr, littleEndianmBIH.xppm); + ENCODE(&mImageBufferCurr, littleEndianmBIH.yppm); + ENCODE(&mImageBufferCurr, littleEndianmBIH.colors); + ENCODE(&mImageBufferCurr, littleEndianmBIH.important_colors); + + if (mBMPInfoHeader.bihsize > InfoHeaderLength::WIN_V3) { + ENCODE(&mImageBufferCurr, littleEndianmBIH.red_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.green_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.blue_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.alpha_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.color_space); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_red); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_green); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_blue); + ENCODE(&mImageBufferCurr, littleEndianmBIH.intent); + ENCODE(&mImageBufferCurr, littleEndianmBIH.profile_offset); + ENCODE(&mImageBufferCurr, littleEndianmBIH.profile_size); + ENCODE(&mImageBufferCurr, littleEndianmBIH.reserved); + } +} + +// Sets a pixel in the image buffer that doesn't have alpha data +static inline void SetPixel24(uint8_t*& imageBufferCurr, uint8_t aRed, + uint8_t aGreen, uint8_t aBlue) { + *imageBufferCurr = aBlue; + *(imageBufferCurr + 1) = aGreen; + *(imageBufferCurr + 2) = aRed; +} + +// Sets a pixel in the image buffer with alpha data +static inline void SetPixel32(uint8_t*& imageBufferCurr, uint8_t aRed, + uint8_t aGreen, uint8_t aBlue, + uint8_t aAlpha = 0xFF) { + *imageBufferCurr = aBlue; + *(imageBufferCurr + 1) = aGreen; + *(imageBufferCurr + 2) = aRed; + *(imageBufferCurr + 3) = aAlpha; +} + +// Encodes a row of image data which does not have alpha data +void nsBMPEncoder::EncodeImageDataRow24(const uint8_t* aData) { + for (int32_t x = 0; x < mBMPInfoHeader.width; x++) { + uint32_t pos = x * BytesPerPixel(mBMPInfoHeader.bpp); + SetPixel24(mImageBufferCurr, aData[pos], aData[pos + 1], aData[pos + 2]); + mImageBufferCurr += BytesPerPixel(mBMPInfoHeader.bpp); + } + + for (uint32_t x = 0; + x < PaddingBytes(mBMPInfoHeader.bpp, mBMPInfoHeader.width); x++) { + *mImageBufferCurr++ = 0; + } +} + +// Encodes a row of image data which does have alpha data +void nsBMPEncoder::EncodeImageDataRow32(const uint8_t* aData) { + for (int32_t x = 0; x < mBMPInfoHeader.width; x++) { + uint32_t pos = x * BytesPerPixel(mBMPInfoHeader.bpp); + SetPixel32(mImageBufferCurr, aData[pos], aData[pos + 1], aData[pos + 2], + aData[pos + 3]); + mImageBufferCurr += 4; + } + + for (uint32_t x = 0; + x < PaddingBytes(mBMPInfoHeader.bpp, mBMPInfoHeader.width); x++) { + *mImageBufferCurr++ = 0; + } +} diff --git a/image/encoders/bmp/nsBMPEncoder.h b/image/encoders/bmp/nsBMPEncoder.h new file mode 100644 index 0000000000..1f40d9df34 --- /dev/null +++ b/image/encoders/bmp/nsBMPEncoder.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_bmp_nsBMPEncoder_h +#define mozilla_image_encoders_bmp_nsBMPEncoder_h + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/UniquePtr.h" + +#include "imgIEncoder.h" + +#include "nsCOMPtr.h" + +#define NS_BMPENCODER_CID \ + { /* 13a5320c-4c91-4FA4-bd16-b081a3ba8c0b */ \ + 0x13a5320c, 0x4c91, 0x4fa4, { \ + 0xbd, 0x16, 0xb0, 0x81, 0xa3, 0Xba, 0x8c, 0x0b \ + } \ + } + +namespace mozilla { +namespace image { +namespace bmp { + +struct FileHeader { + char signature[2]; // String "BM". + uint32_t filesize; // File size. + int32_t reserved; // Zero. + uint32_t dataoffset; // Offset to raster data. +}; + +struct XYZ { + int32_t x, y, z; +}; + +struct XYZTriple { + XYZ r, g, b; +}; + +struct V5InfoHeader { + uint32_t bihsize; // Header size + int32_t width; // Uint16 in OS/2 BMPs + int32_t height; // Uint16 in OS/2 BMPs + uint16_t planes; // =1 + uint16_t bpp; // Bits per pixel. + uint32_t compression; // See Compression for valid values + uint32_t image_size; // (compressed) image size. Can be 0 if + // compression==0 + uint32_t xppm; // Pixels per meter, horizontal + uint32_t yppm; // Pixels per meter, vertical + uint32_t colors; // Used Colors + uint32_t important_colors; // Number of important colors. 0=all + // The rest of the header is not available in WIN_V3 BMP Files + uint32_t red_mask; // Bits used for red component + uint32_t green_mask; // Bits used for green component + uint32_t blue_mask; // Bits used for blue component + uint32_t alpha_mask; // Bits used for alpha component + uint32_t color_space; // 0x73524742=LCS_sRGB ... + // These members are unused unless color_space == LCS_CALIBRATED_RGB + XYZTriple white_point; // Logical white point + uint32_t gamma_red; // Red gamma component + uint32_t gamma_green; // Green gamma component + uint32_t gamma_blue; // Blue gamma component + uint32_t intent; // Rendering intent + // These members are unused unless color_space == LCS_PROFILE_* + uint32_t profile_offset; // Offset to profile data in bytes + uint32_t profile_size; // Size of profile data in bytes + uint32_t reserved; // =0 + + static const uint32_t COLOR_SPACE_LCS_SRGB = 0x73524742; +}; + +} // namespace bmp +} // namespace image +} // namespace mozilla + +// Provides BMP encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsBMPEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsBMPEncoder(); + + protected: + ~nsBMPEncoder(); + + enum Version { VERSION_3 = 3, VERSION_5 = 5 }; + + // See InitData in the cpp for valid parse options + nsresult ParseOptions(const nsAString& aOptions, Version& aVersionOut, + uint16_t& aBppOut); + // Obtains data with no alpha in machine-independent byte order + void ConvertHostARGBRow(const uint8_t* aSrc, + const mozilla::UniquePtr& aDest, + uint32_t aPixelWidth); + // Thread safe notify listener + void NotifyListener(); + + // Initializes the bitmap file header member mBMPFileHeader + nsresult InitFileHeader(Version aVersion, uint16_t aBPP, uint32_t aWidth, + uint32_t aHeight); + // Initializes the bitmap info header member mBMPInfoHeader + nsresult InitInfoHeader(Version aVersion, uint16_t aBPP, uint32_t aWidth, + uint32_t aHeight); + + // Encodes the bitmap file header member mBMPFileHeader + void EncodeFileHeader(); + // Encodes the bitmap info header member mBMPInfoHeader + void EncodeInfoHeader(); + // Encodes a row of image data which does not have alpha data + void EncodeImageDataRow24(const uint8_t* aData); + // Encodes a row of image data which does have alpha data + void EncodeImageDataRow32(const uint8_t* aData); + // Obtains the current offset filled up to for the image buffer + inline int32_t GetCurrentImageBufferOffset() { + return static_cast(mImageBufferCurr - mImageBufferStart); + } + + // These headers will always contain endian independent stuff + // They store the BMP headers which will be encoded + mozilla::image::bmp::FileHeader mBMPFileHeader; + mozilla::image::bmp::V5InfoHeader mBMPInfoHeader; + + // Keeps track of the start of the image buffer + uint8_t* mImageBufferStart; + // Keeps track of the current position in the image buffer + uint8_t* mImageBufferCurr; + // Keeps track of the image buffer size + uint32_t mImageBufferSize; + // Keeps track of the number of bytes in the image buffer which are read + uint32_t mImageBufferReadPoint; + // Stores true if the image is done being encoded + bool mFinished; + + nsCOMPtr mCallback; + nsCOMPtr mCallbackTarget; + uint32_t mNotifyThreshold; +}; + +#endif // mozilla_image_encoders_bmp_nsBMPEncoder_h diff --git a/image/encoders/ico/moz.build b/image/encoders/ico/moz.build new file mode 100644 index 0000000000..27e41489fd --- /dev/null +++ b/image/encoders/ico/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsICOEncoder.cpp", +] + +# Decoders need RasterImage.h +LOCAL_INCLUDES += [ + "/image", + "/image/encoders/bmp", + "/image/encoders/png", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/ico/nsICOEncoder.cpp b/image/encoders/ico/nsICOEncoder.cpp new file mode 100644 index 0000000000..518acc9a0b --- /dev/null +++ b/image/encoders/ico/nsICOEncoder.cpp @@ -0,0 +1,499 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCRT.h" +#include "mozilla/EndianUtils.h" +#include "nsBMPEncoder.h" +#include "BMPHeaders.h" +#include "nsPNGEncoder.h" +#include "nsICOEncoder.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "nsTArray.h" + +using namespace mozilla; +using namespace mozilla::image; + +NS_IMPL_ISUPPORTS(nsICOEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +nsICOEncoder::nsICOEncoder() + : mICOFileHeader{}, + mICODirEntry{}, + mImageBufferStart(nullptr), + mImageBufferCurr(0), + mImageBufferSize(0), + mImageBufferReadPoint(0), + mFinished(false), + mUsePNG(true), + mNotifyThreshold(0) {} + +nsICOEncoder::~nsICOEncoder() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferCurr = nullptr; + } +} + +// nsICOEncoder::InitFromData +// Two output options are supported: format=;bpp= +// format specifies whether to use png or bitmap format +// bpp specifies the bits per pixel to use where bpp_value can be 24 or 32 +NS_IMETHODIMP +nsICOEncoder::InitFromData(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + // Stride is the padded width of each row, so it better be longer + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData"); + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EndImageEncode(); + return rv; +} + +// Returns the number of bytes in the image buffer used +// For an ICO file, this is all bytes in the buffer. +NS_IMETHODIMP +nsICOEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferSize; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsICOEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast(mImageBufferStart); + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::AddImageFrame(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aFrameOptions) { + if (mUsePNG) { + mContainedEncoder = new nsPNGEncoder(); + nsresult rv; + nsAutoString noParams; + rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight, + aStride, aInputFormat, noParams); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t PNGImageBufferSize; + mContainedEncoder->GetImageBufferUsed(&PNGImageBufferSize); + mImageBufferSize = + ICONFILEHEADERSIZE + ICODIRENTRYSIZE + PNGImageBufferSize; + mImageBufferStart = static_cast(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + mICODirEntry.mBytesInRes = PNGImageBufferSize; + + EncodeFileHeader(); + EncodeInfoHeader(); + + char* imageBuffer; + rv = mContainedEncoder->GetImageBuffer(&imageBuffer); + NS_ENSURE_SUCCESS(rv, rv); + memcpy(mImageBufferCurr, imageBuffer, PNGImageBufferSize); + mImageBufferCurr += PNGImageBufferSize; + } else { + mContainedEncoder = new nsBMPEncoder(); + nsresult rv; + + nsAutoString params; + params.AppendLiteral("bpp="); + params.AppendInt(mICODirEntry.mBitCount); + + rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight, + aStride, aInputFormat, params); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t andMaskSize = ((GetRealWidth() + 31) / 32) * 4 * // row AND mask + GetRealHeight(); // num rows + + uint32_t BMPImageBufferSize; + mContainedEncoder->GetImageBufferUsed(&BMPImageBufferSize); + mImageBufferSize = + ICONFILEHEADERSIZE + ICODIRENTRYSIZE + BMPImageBufferSize + andMaskSize; + mImageBufferStart = static_cast(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + + // Icon files that wrap a BMP file must not include the BITMAPFILEHEADER + // section at the beginning of the encoded BMP data, so we must skip over + // bmp::FILE_HEADER_LENGTH bytes when adding the BMP content to the icon + // file. + mICODirEntry.mBytesInRes = + BMPImageBufferSize - bmp::FILE_HEADER_LENGTH + andMaskSize; + + // Encode the icon headers + EncodeFileHeader(); + EncodeInfoHeader(); + + char* imageBuffer; + rv = mContainedEncoder->GetImageBuffer(&imageBuffer); + NS_ENSURE_SUCCESS(rv, rv); + memcpy(mImageBufferCurr, imageBuffer + bmp::FILE_HEADER_LENGTH, + BMPImageBufferSize - bmp::FILE_HEADER_LENGTH); + // We need to fix the BMP height to be *2 for the AND mask + uint32_t fixedHeight = GetRealHeight() * 2; + NativeEndian::swapToLittleEndianInPlace(&fixedHeight, 1); + // The height is stored at an offset of 8 from the DIB header + memcpy(mImageBufferCurr + 8, &fixedHeight, sizeof(fixedHeight)); + mImageBufferCurr += BMPImageBufferSize - bmp::FILE_HEADER_LENGTH; + + // Calculate rowsize in DWORD's + uint32_t rowSize = ((GetRealWidth() + 31) / 32) * 4; // + 31 to round up + int32_t currentLine = GetRealHeight(); + + // Write out the AND mask + while (currentLine > 0) { + currentLine--; + uint8_t* encoded = mImageBufferCurr + currentLine * rowSize; + uint8_t* encodedEnd = encoded + rowSize; + while (encoded != encodedEnd) { + *encoded = 0; // make everything visible + encoded++; + } + } + + mImageBufferCurr += andMaskSize; + } + + return NS_OK; +} + +// See ::InitFromData for other info. +NS_IMETHODIMP +nsICOEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // can't initialize more than once + if (mImageBufferStart || mImageBufferCurr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + // Icons are only 1 byte, so make sure our bitmap is in range + if (aWidth > 256 || aHeight > 256) { + return NS_ERROR_INVALID_ARG; + } + + // parse and check any provided output options + uint16_t bpp = 24; + bool usePNG = true; + nsresult rv = ParseOptions(aOutputOptions, bpp, usePNG); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(bpp <= 32); + + mUsePNG = usePNG; + + InitFileHeader(); + // The width and height are stored as 0 when we have a value of 256 + InitInfoHeader(bpp, aWidth == 256 ? 0 : (uint8_t)aWidth, + aHeight == 256 ? 0 : (uint8_t)aHeight); + + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::EndImageEncode() { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +// Parses the encoder options and sets the bits per pixel to use and PNG or BMP +// See InitFromData for a description of the parse options +nsresult nsICOEncoder::ParseOptions(const nsAString& aOptions, + uint16_t& aBppOut, bool& aUsePNGOut) { + // If no parsing options just use the default of 24BPP and PNG yes + if (aOptions.Length() == 0) { + aUsePNGOut = true; + aBppOut = 24; + } + + // Parse the input string into a set of name/value pairs. + // From format: format=;bpp= + // to format: [0] = format=, [1] = bpp= + nsTArray nameValuePairs; + ParseString(NS_ConvertUTF16toUTF8(aOptions), ';', nameValuePairs); + + // For each name/value pair in the set + for (unsigned i = 0; i < nameValuePairs.Length(); ++i) { + // Split the name value pair [0] = name, [1] = value + nsTArray nameValuePair; + ParseString(nameValuePairs[i], '=', nameValuePair); + if (nameValuePair.Length() != 2) { + return NS_ERROR_INVALID_ARG; + } + + // Parse the format portion of the string format=;bpp= + if (nameValuePair[0].Equals("format", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].Equals("png", nsCaseInsensitiveCStringComparator)) { + aUsePNGOut = true; + } else if (nameValuePair[1].Equals("bmp", + nsCaseInsensitiveCStringComparator)) { + aUsePNGOut = false; + } else { + return NS_ERROR_INVALID_ARG; + } + } + + // Parse the bpp portion of the string format=;bpp= + if (nameValuePair[0].Equals("bpp", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("24")) { + aBppOut = 24; + } else if (nameValuePair[1].EqualsLiteral("32")) { + aBppOut = 32; + } else { + return NS_ERROR_INVALID_ARG; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::Close() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferSize = 0; + mImageBufferReadPoint = 0; + mImageBufferCurr = nullptr; + } + + return NS_OK; +} + +// Obtains the available bytes to read +NS_IMETHODIMP +nsICOEncoder::Available(uint64_t* _retval) { + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + return NS_OK; +} + +// Obtains the stream's status +NS_IMETHODIMP +nsICOEncoder::StreamStatus() { + return mImageBufferStart && mImageBufferCurr ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +// [noscript] Reads bytes which are available +NS_IMETHODIMP +nsICOEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +// [noscript] Reads segments +NS_IMETHODIMP +nsICOEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + uint32_t maxCount = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast(mImageBufferStart + mImageBufferReadPoint), + 0, aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // We don't want to notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the + // main thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +void nsICOEncoder::NotifyListener() { + if (mCallback && (GetCurrentImageBufferOffset() - mImageBufferReadPoint >= + mNotifyThreshold || + mFinished)) { + nsCOMPtr callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsICOEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could reenter + // AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} + +// Initializes the icon file header mICOFileHeader +void nsICOEncoder::InitFileHeader() { + memset(&mICOFileHeader, 0, sizeof(mICOFileHeader)); + mICOFileHeader.mReserved = 0; + mICOFileHeader.mType = 1; + mICOFileHeader.mCount = 1; +} + +// Initializes the icon directory info header mICODirEntry +void nsICOEncoder::InitInfoHeader(uint16_t aBPP, uint8_t aWidth, + uint8_t aHeight) { + memset(&mICODirEntry, 0, sizeof(mICODirEntry)); + mICODirEntry.mBitCount = aBPP; + mICODirEntry.mBytesInRes = 0; + mICODirEntry.mColorCount = 0; + mICODirEntry.mWidth = aWidth; + mICODirEntry.mHeight = aHeight; + mICODirEntry.mImageOffset = ICONFILEHEADERSIZE + ICODIRENTRYSIZE; + mICODirEntry.mPlanes = 1; + mICODirEntry.mReserved = 0; +} + +// Encodes the icon file header mICOFileHeader +void nsICOEncoder::EncodeFileHeader() { + IconFileHeader littleEndianIFH = mICOFileHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mReserved, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mType, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mCount, 1); + + memcpy(mImageBufferCurr, &littleEndianIFH.mReserved, + sizeof(littleEndianIFH.mReserved)); + mImageBufferCurr += sizeof(littleEndianIFH.mReserved); + memcpy(mImageBufferCurr, &littleEndianIFH.mType, + sizeof(littleEndianIFH.mType)); + mImageBufferCurr += sizeof(littleEndianIFH.mType); + memcpy(mImageBufferCurr, &littleEndianIFH.mCount, + sizeof(littleEndianIFH.mCount)); + mImageBufferCurr += sizeof(littleEndianIFH.mCount); +} + +// Encodes the icon directory info header mICODirEntry +void nsICOEncoder::EncodeInfoHeader() { + IconDirEntry littleEndianmIDE = mICODirEntry; + + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mPlanes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBitCount, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBytesInRes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mImageOffset, 1); + + memcpy(mImageBufferCurr, &littleEndianmIDE.mWidth, + sizeof(littleEndianmIDE.mWidth)); + mImageBufferCurr += sizeof(littleEndianmIDE.mWidth); + memcpy(mImageBufferCurr, &littleEndianmIDE.mHeight, + sizeof(littleEndianmIDE.mHeight)); + mImageBufferCurr += sizeof(littleEndianmIDE.mHeight); + memcpy(mImageBufferCurr, &littleEndianmIDE.mColorCount, + sizeof(littleEndianmIDE.mColorCount)); + mImageBufferCurr += sizeof(littleEndianmIDE.mColorCount); + memcpy(mImageBufferCurr, &littleEndianmIDE.mReserved, + sizeof(littleEndianmIDE.mReserved)); + mImageBufferCurr += sizeof(littleEndianmIDE.mReserved); + memcpy(mImageBufferCurr, &littleEndianmIDE.mPlanes, + sizeof(littleEndianmIDE.mPlanes)); + mImageBufferCurr += sizeof(littleEndianmIDE.mPlanes); + memcpy(mImageBufferCurr, &littleEndianmIDE.mBitCount, + sizeof(littleEndianmIDE.mBitCount)); + mImageBufferCurr += sizeof(littleEndianmIDE.mBitCount); + memcpy(mImageBufferCurr, &littleEndianmIDE.mBytesInRes, + sizeof(littleEndianmIDE.mBytesInRes)); + mImageBufferCurr += sizeof(littleEndianmIDE.mBytesInRes); + memcpy(mImageBufferCurr, &littleEndianmIDE.mImageOffset, + sizeof(littleEndianmIDE.mImageOffset)); + mImageBufferCurr += sizeof(littleEndianmIDE.mImageOffset); +} diff --git a/image/encoders/ico/nsICOEncoder.h b/image/encoders/ico/nsICOEncoder.h new file mode 100644 index 0000000000..ef601ba97e --- /dev/null +++ b/image/encoders/ico/nsICOEncoder.h @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_ico_nsICOEncoder_h +#define mozilla_image_encoders_ico_nsICOEncoder_h + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/image/ICOFileHeaders.h" + +#include "imgIEncoder.h" + +#include "nsCOMPtr.h" + +#define NS_ICOENCODER_CID \ + { /*92AE3AB2-8968-41B1-8709-B6123BCEAF21 */ \ + 0x92ae3ab2, 0x8968, 0x41b1, { \ + 0x87, 0x09, 0xb6, 0x12, 0x3b, 0Xce, 0xaf, 0x21 \ + } \ + } + +// Provides ICO encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsICOEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsICOEncoder(); + + // Obtains the width of the icon directory entry + uint32_t GetRealWidth() const { + return mICODirEntry.mWidth == 0 ? 256 : mICODirEntry.mWidth; + } + + // Obtains the height of the icon directory entry + uint32_t GetRealHeight() const { + return mICODirEntry.mHeight == 0 ? 256 : mICODirEntry.mHeight; + } + + protected: + ~nsICOEncoder(); + + nsresult ParseOptions(const nsAString& aOptions, uint16_t& aBppOut, + bool& aUsePNGOut); + void NotifyListener(); + + // Initializes the icon file header mICOFileHeader + void InitFileHeader(); + // Initializes the icon directory info header mICODirEntry + void InitInfoHeader(uint16_t aBPP, uint8_t aWidth, uint8_t aHeight); + // Encodes the icon file header mICOFileHeader + void EncodeFileHeader(); + // Encodes the icon directory info header mICODirEntry + void EncodeInfoHeader(); + // Obtains the current offset filled up to for the image buffer + inline int32_t GetCurrentImageBufferOffset() { + return static_cast(mImageBufferCurr - mImageBufferStart); + } + + // Holds either a PNG or a BMP depending on the encoding options specified + // or if no encoding options specified will use the default (PNG) + nsCOMPtr mContainedEncoder; + + // These headers will always contain endian independent stuff. + // Don't trust the width and height of mICODirEntry directly, + // instead use the accessors GetRealWidth() and GetRealHeight(). + mozilla::image::IconFileHeader mICOFileHeader; + mozilla::image::IconDirEntry mICODirEntry; + + // Keeps track of the start of the image buffer + uint8_t* mImageBufferStart; + // Keeps track of the current position in the image buffer + uint8_t* mImageBufferCurr; + // Keeps track of the image buffer size + uint32_t mImageBufferSize; + // Keeps track of the number of bytes in the image buffer which are read + uint32_t mImageBufferReadPoint; + // Stores true if the image is done being encoded + bool mFinished; + // Stores true if the contained image is a PNG + bool mUsePNG; + + nsCOMPtr mCallback; + nsCOMPtr mCallbackTarget; + uint32_t mNotifyThreshold; +}; + +#endif // mozilla_image_encoders_ico_nsICOEncoder_h diff --git a/image/encoders/jpeg/moz.build b/image/encoders/jpeg/moz.build new file mode 100644 index 0000000000..6952fc1b9f --- /dev/null +++ b/image/encoders/jpeg/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsJPEGEncoder.cpp", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/jpeg/nsJPEGEncoder.cpp b/image/encoders/jpeg/nsJPEGEncoder.cpp new file mode 100644 index 0000000000..2975532b58 --- /dev/null +++ b/image/encoders/jpeg/nsJPEGEncoder.cpp @@ -0,0 +1,519 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsJPEGEncoder.h" +#include "prprf.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "gfxColor.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/UniquePtrExtensions.h" + +extern "C" { +#include "jpeglib.h" +} + +#include +#include "jerror.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsJPEGEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +class nsJPEGEncoderInternal { + friend class nsJPEGEncoder; + + protected: + /** + * Initialize destination. This is called by jpeg_start_compress() before + * any data is actually written. It must initialize next_output_byte and + * free_in_buffer. free_in_buffer must be initialized to a positive value. + */ + static void initDestination(jpeg_compress_struct* cinfo); + + /** + * This is called whenever the buffer has filled (free_in_buffer reaches + * zero). In typical applications, it should write out the *entire* buffer + * (use the saved start address and buffer length; ignore the current state + * of next_output_byte and free_in_buffer). Then reset the pointer & count + * to the start of the buffer, and return TRUE indicating that the buffer + * has been dumped. free_in_buffer must be set to a positive value when + * TRUE is returned. A FALSE return should only be used when I/O suspension + * is desired (this operating mode is discussed in the next section). + */ + static boolean emptyOutputBuffer(jpeg_compress_struct* cinfo); + + /** + * Terminate destination --- called by jpeg_finish_compress() after all data + * has been written. In most applications, this must flush any data + * remaining in the buffer. Use either next_output_byte or free_in_buffer + * to determine how much data is in the buffer. + */ + static void termDestination(jpeg_compress_struct* cinfo); + + /** + * Override the standard error method in the IJG JPEG decoder code. This + * was mostly copied from nsJPEGDecoder.cpp + */ + static void errorExit(jpeg_common_struct* cinfo); +}; + +// used to pass error info through the JPEG library +struct encoder_error_mgr { + jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +nsJPEGEncoder::nsJPEGEncoder() + : mFinished(false), + mImageBuffer(nullptr), + mImageBufferSize(0), + mImageBufferUsed(0), + mImageBufferReadPoint(0), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0), + mReentrantMonitor("nsJPEGEncoder.mReentrantMonitor") {} + +nsJPEGEncoder::~nsJPEGEncoder() { + if (mImageBuffer) { + free(mImageBuffer); + mImageBuffer = nullptr; + } +} + +// nsJPEGEncoder::InitFromData +// +// One output option is supported: "quality=X" where X is an integer in the +// range 0-100. Higher values for X give better quality. +// +// Transparency is always discarded. + +NS_IMETHODIMP +nsJPEGEncoder::InitFromData(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + NS_ENSURE_ARG(aData); + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // Stride is the padded width of each row, so it better be longer (I'm afraid + // people will not understand what stride means, so check it well) + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData"); + return NS_ERROR_INVALID_ARG; + } + + // can't initialize more than once + if (mImageBuffer != nullptr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // options: we only have one option so this is easy + int quality = 92; + if (aOutputOptions.Length() > 0) { + // have options string + const nsString qualityPrefix(u"quality="_ns); + if (aOutputOptions.Length() > qualityPrefix.Length() && + StringBeginsWith(aOutputOptions, qualityPrefix)) { + // have quality string + nsCString value = NS_ConvertUTF16toUTF8( + Substring(aOutputOptions, qualityPrefix.Length())); + int newquality = -1; + if (PR_sscanf(value.get(), "%d", &newquality) == 1) { + if (newquality >= 0 && newquality <= 100) { + quality = newquality; + } else { + NS_WARNING( + "Quality value out of range, should be 0-100," + " using default"); + } + } else { + NS_WARNING( + "Quality value invalid, should be integer 0-100," + " using default"); + } + } else { + return NS_ERROR_INVALID_ARG; + } + } + + UniquePtr rowptr; + if (aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) { + rowptr = MakeUniqueFallible(aWidth * 3); + if (NS_WARN_IF(!rowptr)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + jpeg_compress_struct cinfo; + + // We set up the normal JPEG error routines, then override error_exit. + // This must be done before the call to create_compress + encoder_error_mgr errmgr; + cinfo.err = jpeg_std_error(&errmgr.pub); + errmgr.pub.error_exit = nsJPEGEncoderInternal::errorExit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(errmgr.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + return NS_ERROR_FAILURE; + } + + jpeg_create_compress(&cinfo); + cinfo.image_width = aWidth; + cinfo.image_height = aHeight; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + cinfo.data_precision = 8; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, 1); // quality here is 0-100 + if (quality >= 90) { + int i; + for (i = 0; i < MAX_COMPONENTS; i++) { + cinfo.comp_info[i].h_samp_factor = 1; + cinfo.comp_info[i].v_samp_factor = 1; + } + } + + // set up the destination manager + jpeg_destination_mgr destmgr; + destmgr.init_destination = nsJPEGEncoderInternal::initDestination; + destmgr.empty_output_buffer = nsJPEGEncoderInternal::emptyOutputBuffer; + destmgr.term_destination = nsJPEGEncoderInternal::termDestination; + cinfo.dest = &destmgr; + cinfo.client_data = this; + + jpeg_start_compress(&cinfo, 1); + + // feed it the rows + if (aInputFormat == INPUT_FORMAT_RGB) { + while (cinfo.next_scanline < cinfo.image_height) { + const uint8_t* row = &aData[cinfo.next_scanline * aStride]; + jpeg_write_scanlines(&cinfo, const_cast(&row), 1); + } + } else if (aInputFormat == INPUT_FORMAT_RGBA) { + MOZ_ASSERT(rowptr); + uint8_t* row = rowptr.get(); + while (cinfo.next_scanline < cinfo.image_height) { + ConvertRGBARow(&aData[cinfo.next_scanline * aStride], row, aWidth); + jpeg_write_scanlines(&cinfo, &row, 1); + } + } else if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + MOZ_ASSERT(rowptr); + uint8_t* row = rowptr.get(); + while (cinfo.next_scanline < cinfo.image_height) { + ConvertHostARGBRow(&aData[cinfo.next_scanline * aStride], row, aWidth); + jpeg_write_scanlines(&cinfo, &row, 1); + } + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBuffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// Returns the number of bytes in the image buffer used. +NS_IMETHODIMP +nsJPEGEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferUsed; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsJPEGEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast(mImageBuffer); + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::AddImageFrame(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, + uint32_t aStride, uint32_t aFrameFormat, + const nsAString& aFrameOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsJPEGEncoder::EndImageEncode() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsJPEGEncoder::Close() { + if (mImageBuffer != nullptr) { + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::Available(uint64_t* _retval) { + if (!mImageBuffer) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mImageBufferUsed - mImageBufferReadPoint; + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::StreamStatus() { + return mImageBuffer ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +NS_IMETHODIMP +nsJPEGEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +nsJPEGEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + // Avoid another thread reallocing the buffer underneath us + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + uint32_t maxCount = mImageBufferUsed - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast(mImageBuffer + mImageBufferReadPoint), 0, + aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // 1 KB seems good. We don't want to + // notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the + // main thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsJPEGEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but we need +// an output with no alpha in machine-independent byte order. +// +// See gfx/cairo/cairo/src/cairo-png.c +void nsJPEGEncoder::ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * 3]; + + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff) >> 0; + } +} + +/** + * nsJPEGEncoder::ConvertRGBARow + * + * Input is RGBA, output is RGB, so we should alpha-premultiply. + */ +void nsJPEGEncoder::ConvertRGBARow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint8_t* pixelIn = &aSrc[x * 4]; + uint8_t* pixelOut = &aDest[x * 3]; + + uint8_t alpha = pixelIn[3]; + pixelOut[0] = gfxPreMultiply(pixelIn[0], alpha); + pixelOut[1] = gfxPreMultiply(pixelIn[1], alpha); + pixelOut[2] = gfxPreMultiply(pixelIn[2], alpha); + } +} + +void nsJPEGEncoder::NotifyListener() { + // We might call this function on multiple threads (any threads that call + // AsyncWait and any that do encoding) so we lock to avoid notifying the + // listener twice about the same data (which generally leads to a truncated + // image). + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + if (mCallback && + (mImageBufferUsed - mImageBufferReadPoint >= mNotifyThreshold || + mFinished)) { + nsCOMPtr callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsJPEGEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could reenter + // AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} + +/* static */ +void nsJPEGEncoderInternal::initDestination(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast(cinfo->client_data); + NS_ASSERTION(!that->mImageBuffer, "Image buffer already initialized"); + + that->mImageBufferSize = 8192; + that->mImageBuffer = (uint8_t*)malloc(that->mImageBufferSize); + that->mImageBufferUsed = 0; + + cinfo->dest->next_output_byte = that->mImageBuffer; + cinfo->dest->free_in_buffer = that->mImageBufferSize; +} + +/* static */ +boolean nsJPEGEncoderInternal::emptyOutputBuffer(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast(cinfo->client_data); + NS_ASSERTION(that->mImageBuffer, "No buffer to empty!"); + + // When we're reallocing the buffer we need to take the lock to ensure + // that nobody is trying to read from the buffer we are destroying + ReentrantMonitorAutoEnter autoEnter(that->mReentrantMonitor); + + that->mImageBufferUsed = that->mImageBufferSize; + + // expand buffer, just double size each time + uint8_t* newBuf = nullptr; + CheckedInt bufSize = + CheckedInt(that->mImageBufferSize) * 2; + if (bufSize.isValid()) { + that->mImageBufferSize = bufSize.value(); + newBuf = (uint8_t*)realloc(that->mImageBuffer, that->mImageBufferSize); + } + + if (!newBuf) { + // can't resize, just zero (this will keep us from writing more) + free(that->mImageBuffer); + that->mImageBuffer = nullptr; + that->mImageBufferSize = 0; + that->mImageBufferUsed = 0; + + // This seems to be the only way to do errors through the JPEG library. We + // pass an nsresult masquerading as an int, which works because the + // setjmp() caller casts it back. + longjmp(((encoder_error_mgr*)(cinfo->err))->setjmp_buffer, + static_cast(NS_ERROR_OUT_OF_MEMORY)); + } + that->mImageBuffer = newBuf; + + cinfo->dest->next_output_byte = &that->mImageBuffer[that->mImageBufferUsed]; + cinfo->dest->free_in_buffer = that->mImageBufferSize - that->mImageBufferUsed; + return 1; +} + +/* static */ +void nsJPEGEncoderInternal::termDestination(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast(cinfo->client_data); + if (!that->mImageBuffer) { + return; + } + that->mImageBufferUsed = cinfo->dest->next_output_byte - that->mImageBuffer; + NS_ASSERTION(that->mImageBufferUsed < that->mImageBufferSize, + "JPEG library busted, got a bad image buffer size"); + that->NotifyListener(); +} + +/* static */ +void nsJPEGEncoderInternal::errorExit(jpeg_common_struct* cinfo) { + nsresult error_code; + encoder_error_mgr* err = (encoder_error_mgr*)cinfo->err; + + // Convert error to a browser error code + switch (cinfo->err->msg_code) { + case JERR_OUT_OF_MEMORY: + error_code = NS_ERROR_OUT_OF_MEMORY; + break; + default: + error_code = NS_ERROR_FAILURE; + } + + // Return control to the setjmp point. We pass an nsresult masquerading as + // an int, which works because the setjmp() caller casts it back. + longjmp(err->setjmp_buffer, static_cast(error_code)); +} diff --git a/image/encoders/jpeg/nsJPEGEncoder.h b/image/encoders/jpeg/nsJPEGEncoder.h new file mode 100644 index 0000000000..df543d957b --- /dev/null +++ b/image/encoders/jpeg/nsJPEGEncoder.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_jpeg_nsJPEGEncoder_h +#define mozilla_image_encoders_jpeg_nsJPEGEncoder_h + +#include "imgIEncoder.h" + +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" + +struct jpeg_compress_struct; +struct jpeg_common_struct; + +#define NS_JPEGENCODER_CID \ + { \ + /* ac2bb8fe-eeeb-4572-b40f-be03932b56e0 */ \ + 0xac2bb8fe, 0xeeeb, 0x4572, { \ + 0xb4, 0x0f, 0xbe, 0x03, 0x93, 0x2b, 0x56, 0xe0 \ + } \ + } + +// Provides JPEG encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. +class nsJPEGEncoderInternal; + +class nsJPEGEncoder final : public imgIEncoder { + friend class nsJPEGEncoderInternal; + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsJPEGEncoder(); + + private: + ~nsJPEGEncoder(); + + protected: + void ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth); + void ConvertRGBARow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth); + + void NotifyListener(); + + bool mFinished; + + // image buffer + uint8_t* mImageBuffer; + uint32_t mImageBufferSize; + uint32_t mImageBufferUsed; + + uint32_t mImageBufferReadPoint; + + nsCOMPtr mCallback; + nsCOMPtr mCallbackTarget; + uint32_t mNotifyThreshold; + + // nsJPEGEncoder is designed to allow one thread to pump data into it while + // another reads from it. We lock to ensure that the buffer remains + // append-only while we read from it (that it is not realloced) and to ensure + // that only one thread dispatches a callback for each call to AsyncWait. + ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; +}; + +#endif // mozilla_image_encoders_jpeg_nsJPEGEncoder_h diff --git a/image/encoders/moz.build b/image/encoders/moz.build new file mode 100644 index 0000000000..ecb2aa44e0 --- /dev/null +++ b/image/encoders/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "ico", + "png", + "jpeg", + "bmp", + "webp", +] diff --git a/image/encoders/png/moz.build b/image/encoders/png/moz.build new file mode 100644 index 0000000000..fe26a2368e --- /dev/null +++ b/image/encoders/png/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsPNGEncoder.cpp", +] + +LOCAL_INCLUDES += [ + "/image", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/png/nsPNGEncoder.cpp b/image/encoders/png/nsPNGEncoder.cpp new file mode 100644 index 0000000000..f21bab67a6 --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.cpp @@ -0,0 +1,820 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" +#include "nsCRT.h" +#include "nsPNGEncoder.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "prprf.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/UniquePtrExtensions.h" + +using namespace mozilla; + +static LazyLogModule sPNGEncoderLog("PNGEncoder"); + +NS_IMPL_ISUPPORTS(nsPNGEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +#define DEFAULT_ZLIB_LEVEL 3 +#define DEFAULT_FILTERS PNG_FILTER_SUB + +nsPNGEncoder::nsPNGEncoder() + : mPNG(nullptr), + mPNGinfo(nullptr), + mIsAnimation(false), + mFinished(false), + mImageBuffer(nullptr), + mImageBufferSize(0), + mImageBufferUsed(0), + mImageBufferReadPoint(0), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0), + mReentrantMonitor("nsPNGEncoder.mReentrantMonitor") {} + +nsPNGEncoder::~nsPNGEncoder() { + if (mImageBuffer) { + free(mImageBuffer); + mImageBuffer = nullptr; + } + // don't leak if EndImageEncode wasn't called + if (mPNG) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + } +} + +// nsPNGEncoder::InitFromData +// +// One output option is supported: "transparency=none" means that the +// output PNG will not have an alpha channel, even if the input does. +// +// Based partially on gfx/cairo/cairo/src/cairo-png.c +// See also media/libpng/libpng-manual.txt + +NS_IMETHODIMP +nsPNGEncoder::InitFromData(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + NS_ENSURE_ARG(aData); + nsresult rv; + + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + if (!NS_SUCCEEDED(rv)) { + return rv; + } + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + if (!NS_SUCCEEDED(rv)) { + return rv; + } + + rv = EndImageEncode(); + + return rv; +} + +// nsPNGEncoder::StartImageEncode +// +// +// See ::InitFromData for other info. +NS_IMETHODIMP +nsPNGEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + bool useTransparency = true, skipFirstFrame = false; + uint32_t numFrames = 1; + uint32_t numPlays = 0; // For animations, 0 == forever + int zlibLevel = DEFAULT_ZLIB_LEVEL; + int filters = DEFAULT_FILTERS; + + // can't initialize more than once + if (mImageBuffer != nullptr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // parse and check any provided output options + nsresult rv = ParseOptions(aOutputOptions, &useTransparency, &skipFirstFrame, + &numFrames, &numPlays, &zlibLevel, &filters, + nullptr, nullptr, nullptr, nullptr, nullptr); + if (rv != NS_OK) { + return rv; + } + +#ifdef PNG_APNG_SUPPORTED + if (numFrames > 1) { + mIsAnimation = true; + } + +#endif + + // initialize + mPNG = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, ErrorCallback, + WarningCallback); + if (!mPNG) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mPNGinfo = png_create_info_struct(mPNG); + if (!mPNGinfo) { + png_destroy_write_struct(&mPNG, nullptr); + return NS_ERROR_FAILURE; + } + + // libpng's error handler jumps back here upon an error. + // Note: It's important that all png_* callers do this, or errors + // will result in a corrupt time-warped stack. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + +#ifdef PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED + png_set_compression_level(mPNG, zlibLevel); +#endif +#ifdef PNG_WRITE_FILTER_SUPPORTED + png_set_filter(mPNG, PNG_FILTER_TYPE_BASE, filters); +#endif + + // Set up to read the data into our image buffer, start out with an 8K + // estimated size. Note: we don't have to worry about freeing this data + // in this function. It will be freed on object destruction. + mImageBufferSize = 8192; + mImageBuffer = (uint8_t*)malloc(mImageBufferSize); + if (!mImageBuffer) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferUsed = 0; + + // set our callback for libpng to give us the data + png_set_write_fn(mPNG, this, WriteCallback, nullptr); + + // include alpha? + int colorType; + if ((aInputFormat == INPUT_FORMAT_HOSTARGB || + aInputFormat == INPUT_FORMAT_RGBA) && + useTransparency) + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + else + colorType = PNG_COLOR_TYPE_RGB; + + png_set_IHDR(mPNG, mPNGinfo, aWidth, aHeight, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + png_set_first_frame_is_hidden(mPNG, mPNGinfo, skipFirstFrame); + png_set_acTL(mPNG, mPNGinfo, numFrames, numPlays); + } +#endif + + // XXX: support PLTE, gAMA, tRNS, bKGD? + + png_write_info(mPNG, mPNGinfo); + + return NS_OK; +} + +// Returns the number of bytes in the image buffer used. +NS_IMETHODIMP +nsPNGEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferUsed; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsPNGEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast(mImageBuffer); + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::AddImageFrame(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aFrameOptions) { + bool useTransparency = true; + int filters = DEFAULT_FILTERS; + uint32_t delay_ms = 500; +#ifdef PNG_APNG_SUPPORTED + uint32_t dispose_op = PNG_DISPOSE_OP_NONE; + uint32_t blend_op = PNG_BLEND_OP_SOURCE; +#else + uint32_t dispose_op; + uint32_t blend_op; +#endif + uint32_t x_offset = 0, y_offset = 0; + + // must be initialized + if (mImageBuffer == nullptr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // EndImageEncode was done, or some error occurred earlier + if (!mPNG) { + return NS_BASE_STREAM_CLOSED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + // parse and check any provided output options + nsresult rv = ParseOptions(aFrameOptions, &useTransparency, nullptr, nullptr, + nullptr, nullptr, &filters, &dispose_op, &blend_op, + &delay_ms, &x_offset, &y_offset); + if (rv != NS_OK) { + return rv; + } + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + // XXX the row pointers arg (#3) is unused, can it be removed? + png_write_frame_head(mPNG, mPNGinfo, nullptr, aWidth, aHeight, x_offset, + y_offset, delay_ms, 1000, dispose_op, blend_op); + } +#endif + + // Stride is the padded width of each row, so it better be longer + // (I'm afraid people will not understand what stride means, so + // check it well) + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData/AddImageFrame"); + return NS_ERROR_INVALID_ARG; + } + +#ifdef PNG_WRITE_FILTER_SUPPORTED + png_set_filter(mPNG, PNG_FILTER_TYPE_BASE, filters); +#endif + + // write each row: if we add more input formats, we may want to + // generalize the conversions + if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + // PNG requires RGBA with post-multiplied alpha, so we need to + // convert + UniquePtr row = MakeUniqueFallible(aWidth * 4); + if (NS_WARN_IF(!row)) { + return NS_ERROR_OUT_OF_MEMORY; + } + for (uint32_t y = 0; y < aHeight; y++) { + ConvertHostARGBRow(&aData[y * aStride], row.get(), aWidth, + useTransparency); + png_write_row(mPNG, row.get()); + } + } else if (aInputFormat == INPUT_FORMAT_RGBA && !useTransparency) { + // RBGA, but we need to strip the alpha + UniquePtr row = MakeUniqueFallible(aWidth * 4); + if (NS_WARN_IF(!row)) { + return NS_ERROR_OUT_OF_MEMORY; + } + for (uint32_t y = 0; y < aHeight; y++) { + StripAlpha(&aData[y * aStride], row.get(), aWidth); + png_write_row(mPNG, row.get()); + } + } else if (aInputFormat == INPUT_FORMAT_RGB || + aInputFormat == INPUT_FORMAT_RGBA) { + // simple RBG(A), no conversion needed + for (uint32_t y = 0; y < aHeight; y++) { + png_write_row(mPNG, (uint8_t*)&aData[y * aStride]); + } + + } else { + MOZ_ASSERT_UNREACHABLE("Bad format type"); + return NS_ERROR_INVALID_ARG; + } + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + png_write_frame_tail(mPNG, mPNGinfo); + } +#endif + + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::EndImageEncode() { + // must be initialized + if (mImageBuffer == nullptr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // EndImageEncode has already been called, or some error + // occurred earlier + if (!mPNG) { + return NS_BASE_STREAM_CLOSED; + } + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + png_write_end(mPNG, mPNGinfo); + png_destroy_write_struct(&mPNG, &mPNGinfo); + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBuffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +nsresult nsPNGEncoder::ParseOptions(const nsAString& aOptions, + bool* useTransparency, bool* skipFirstFrame, + uint32_t* numFrames, uint32_t* numPlays, + int* zlibLevel, int* filters, + uint32_t* frameDispose, + uint32_t* frameBlend, uint32_t* frameDelay, + uint32_t* offsetX, uint32_t* offsetY) { +#ifdef PNG_APNG_SUPPORTED + // Make a copy of aOptions, because strtok() will modify it. + nsAutoCString optionsCopy; + optionsCopy.Assign(NS_ConvertUTF16toUTF8(aOptions)); + char* options = optionsCopy.BeginWriting(); + + while (char* token = nsCRT::strtok(options, ";", &options)) { + // If there's an '=' character, split the token around it. + char* equals = token; + char* value = nullptr; + while (*equals != '=' && *equals) { + ++equals; + } + if (*equals == '=') { + value = equals + 1; + } + + if (value) { + *equals = '\0'; // temporary null + } + + // transparency=[yes|no|none] + if (nsCRT::strcmp(token, "transparency") == 0 && useTransparency) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "none") == 0 || + nsCRT::strcmp(value, "no") == 0) { + *useTransparency = false; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *useTransparency = true; + } else { + return NS_ERROR_INVALID_ARG; + } + + // skipfirstframe=[yes|no] + } else if (nsCRT::strcmp(token, "skipfirstframe") == 0 && skipFirstFrame) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "no") == 0) { + *skipFirstFrame = false; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *skipFirstFrame = true; + } else { + return NS_ERROR_INVALID_ARG; + } + + // frames=# + } else if (nsCRT::strcmp(token, "frames") == 0 && numFrames) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", numFrames) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // frames=0 is nonsense. + if (*numFrames == 0) { + return NS_ERROR_INVALID_ARG; + } + + // plays=# + } else if (nsCRT::strcmp(token, "plays") == 0 && numPlays) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + // plays=0 to loop forever, otherwise play sequence specified + // number of times + if (PR_sscanf(value, "%u", numPlays) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // png-zlib-level=# + } else if (nsCRT::strcmp(token, "png-zlib-level") == 0) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + int localZlibLevel = DEFAULT_ZLIB_LEVEL; + if (PR_sscanf(value, "%d", &localZlibLevel) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // zlib-level 0-9 are the only valid values + if (localZlibLevel < 0 || localZlibLevel > 9) { + return NS_ERROR_INVALID_ARG; + } + + if (zlibLevel) { + *zlibLevel = localZlibLevel; + } + + // png-filter=[no_filters|none|sub|up|avg|paeth|fast|all] + } else if (nsCRT::strcmp(token, "png-filter") == 0) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "no_filters") == 0) { + if (filters) { + *filters = PNG_NO_FILTERS; + } + } else if (nsCRT::strcmp(value, "none") == 0) { + if (filters) { + *filters = PNG_FILTER_NONE; + } + } else if (nsCRT::strcmp(value, "sub") == 0) { + if (filters) { + *filters = PNG_FILTER_SUB; + } + } else if (nsCRT::strcmp(value, "up") == 0) { + if (filters) { + *filters = PNG_FILTER_UP; + } + } else if (nsCRT::strcmp(value, "avg") == 0) { + if (filters) { + *filters = PNG_FILTER_AVG; + } + } else if (nsCRT::strcmp(value, "paeth") == 0) { + if (filters) { + *filters = PNG_FILTER_PAETH; + } + } else if (nsCRT::strcmp(value, "fast") == 0) { + if (filters) { + *filters = PNG_FAST_FILTERS; + } + } else if (nsCRT::strcmp(value, "all") == 0) { + if (filters) { + *filters = PNG_ALL_FILTERS; + } + } else { + return NS_ERROR_INVALID_ARG; + } + + // dispose=[none|background|previous] + } else if (nsCRT::strcmp(token, "dispose") == 0 && frameDispose) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "none") == 0) { + *frameDispose = PNG_DISPOSE_OP_NONE; + } else if (nsCRT::strcmp(value, "background") == 0) { + *frameDispose = PNG_DISPOSE_OP_BACKGROUND; + } else if (nsCRT::strcmp(value, "previous") == 0) { + *frameDispose = PNG_DISPOSE_OP_PREVIOUS; + } else { + return NS_ERROR_INVALID_ARG; + } + + // blend=[source|over] + } else if (nsCRT::strcmp(token, "blend") == 0 && frameBlend) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "source") == 0) { + *frameBlend = PNG_BLEND_OP_SOURCE; + } else if (nsCRT::strcmp(value, "over") == 0) { + *frameBlend = PNG_BLEND_OP_OVER; + } else { + return NS_ERROR_INVALID_ARG; + } + + // delay=# (in ms) + } else if (nsCRT::strcmp(token, "delay") == 0 && frameDelay) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", frameDelay) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // xoffset=# + } else if (nsCRT::strcmp(token, "xoffset") == 0 && offsetX) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", offsetX) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // yoffset=# + } else if (nsCRT::strcmp(token, "yoffset") == 0 && offsetY) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", offsetY) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // unknown token name + } else + return NS_ERROR_INVALID_ARG; + + if (value) { + *equals = '='; // restore '=' so strtok doesn't get lost + } + } + +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::Close() { + if (mImageBuffer != nullptr) { + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::Available(uint64_t* _retval) { + if (!mImageBuffer) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mImageBufferUsed - mImageBufferReadPoint; + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::StreamStatus() { + return mImageBuffer ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +NS_IMETHODIMP +nsPNGEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +nsPNGEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + // Avoid another thread reallocing the buffer underneath us + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + uint32_t maxCount = mImageBufferUsed - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast(mImageBuffer + mImageBufferReadPoint), 0, + aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // We don't want to notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the main + // thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsPNGEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but PNGs use +// post-multiplied alpha. This swaps to PNG-style alpha. +// +// Copied from gfx/cairo/cairo/src/cairo-png.c + +void nsPNGEncoder::ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth, + bool aUseTransparency) { + uint32_t pixelStride = aUseTransparency ? 4 : 3; + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * pixelStride]; + + uint8_t alpha = (pixelIn & 0xff000000) >> 24; + pixelOut[pixelStride - 1] = alpha; // overwritten below if pixelStride == 3 + if (alpha == 255) { + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff); + } else if (alpha == 0) { + pixelOut[0] = pixelOut[1] = pixelOut[2] = 0; + } else { + pixelOut[0] = (((pixelIn & 0xff0000) >> 16) * 255 + alpha / 2) / alpha; + pixelOut[1] = (((pixelIn & 0x00ff00) >> 8) * 255 + alpha / 2) / alpha; + pixelOut[2] = (((pixelIn & 0x0000ff)) * 255 + alpha / 2) / alpha; + } + } +} + +// nsPNGEncoder::StripAlpha +// +// Input is RGBA, output is RGB + +void nsPNGEncoder::StripAlpha(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint8_t* pixelIn = &aSrc[x * 4]; + uint8_t* pixelOut = &aDest[x * 3]; + pixelOut[0] = pixelIn[0]; + pixelOut[1] = pixelIn[1]; + pixelOut[2] = pixelIn[2]; + } +} + +// nsPNGEncoder::WarningCallback + +void nsPNGEncoder::WarningCallback(png_structp png_ptr, + png_const_charp warning_msg) { + MOZ_LOG(sPNGEncoderLog, LogLevel::Warning, + ("libpng warning: %s\n", warning_msg)); +} + +// nsPNGEncoder::ErrorCallback + +void nsPNGEncoder::ErrorCallback(png_structp png_ptr, + png_const_charp error_msg) { + MOZ_LOG(sPNGEncoderLog, LogLevel::Error, ("libpng error: %s\n", error_msg)); + png_longjmp(png_ptr, 1); +} + +// nsPNGEncoder::WriteCallback + +void // static +nsPNGEncoder::WriteCallback(png_structp png, png_bytep data, png_size_t size) { + nsPNGEncoder* that = static_cast(png_get_io_ptr(png)); + if (!that->mImageBuffer) { + return; + } + + CheckedUint32 sizeNeeded = CheckedUint32(that->mImageBufferUsed) + size; + if (!sizeNeeded.isValid()) { + // Take the lock to ensure that nobody is trying to read from the buffer + // we are destroying + ReentrantMonitorAutoEnter autoEnter(that->mReentrantMonitor); + + that->NullOutImageBuffer(); + return; + } + + if (sizeNeeded.value() > that->mImageBufferSize) { + // When we're reallocing the buffer we need to take the lock to ensure + // that nobody is trying to read from the buffer we are destroying + ReentrantMonitorAutoEnter autoEnter(that->mReentrantMonitor); + + while (sizeNeeded.value() > that->mImageBufferSize) { + // expand buffer, just double each time + CheckedUint32 bufferSize = CheckedUint32(that->mImageBufferSize) * 2; + if (!bufferSize.isValid()) { + that->NullOutImageBuffer(); + return; + } + that->mImageBufferSize *= 2; + uint8_t* newBuf = + (uint8_t*)realloc(that->mImageBuffer, that->mImageBufferSize); + if (!newBuf) { + // can't resize, just zero (this will keep us from writing more) + that->NullOutImageBuffer(); + return; + } + that->mImageBuffer = newBuf; + } + } + + memcpy(&that->mImageBuffer[that->mImageBufferUsed], data, size); + that->mImageBufferUsed += size; + that->NotifyListener(); +} + +void nsPNGEncoder::NullOutImageBuffer() { + mReentrantMonitor.AssertCurrentThreadIn(); + + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; +} + +void nsPNGEncoder::NotifyListener() { + // We might call this function on multiple threads (any threads that call + // AsyncWait and any that do encoding) so we lock to avoid notifying the + // listener twice about the same data (which generally leads to a truncated + // image). + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + if (mCallback && + (mImageBufferUsed - mImageBufferReadPoint >= mNotifyThreshold || + mFinished)) { + nsCOMPtr callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsPNGEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could reenter + // AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} diff --git a/image/encoders/png/nsPNGEncoder.h b/image/encoders/png/nsPNGEncoder.h new file mode 100644 index 0000000000..76d695ccfa --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_png_nsPNGEncoder_h +#define mozilla_image_encoders_png_nsPNGEncoder_h + +#include + +#include "imgIEncoder.h" +#include "nsCOMPtr.h" + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" + +#define NS_PNGENCODER_CID \ + { /* 38d1592e-b81e-432b-86f8-471878bbfe07 */ \ + 0x38d1592e, 0xb81e, 0x432b, { \ + 0x86, 0xf8, 0x47, 0x18, 0x78, 0xbb, 0xfe, 0x07 \ + } \ + } + +// Provides PNG encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsPNGEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsPNGEncoder(); + + protected: + ~nsPNGEncoder(); + nsresult ParseOptions(const nsAString& aOptions, bool* useTransparency, + bool* skipFirstFrame, uint32_t* numAnimatedFrames, + uint32_t* numIterations, int* zlibLevel, int* filters, + uint32_t* frameDispose, uint32_t* frameBlend, + uint32_t* frameDelay, uint32_t* offsetX, + uint32_t* offsetY); + void ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth, bool aUseTransparency); + void StripAlpha(const uint8_t* aSrc, uint8_t* aDest, uint32_t aPixelWidth); + static void WarningCallback(png_structp png_ptr, png_const_charp warning_msg); + static void ErrorCallback(png_structp png_ptr, png_const_charp error_msg); + static void WriteCallback(png_structp png, png_bytep data, png_size_t size); + void NullOutImageBuffer(); + void NotifyListener(); + + png_struct* mPNG; + png_info* mPNGinfo; + + bool mIsAnimation; + bool mFinished; + + // image buffer + uint8_t* mImageBuffer; + uint32_t mImageBufferSize; + uint32_t mImageBufferUsed; + + uint32_t mImageBufferReadPoint; + + nsCOMPtr mCallback; + nsCOMPtr mCallbackTarget; + uint32_t mNotifyThreshold; + + // nsPNGEncoder is designed to allow one thread to pump data into it while + // another reads from it. We lock to ensure that the buffer remains + // append-only while we read from it (that it is not realloced) and to + // ensure that only one thread dispatches a callback for each call to + // AsyncWait. + ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; +}; +#endif // mozilla_image_encoders_png_nsPNGEncoder_h diff --git a/image/encoders/webp/moz.build b/image/encoders/webp/moz.build new file mode 100644 index 0000000000..4363caa79f --- /dev/null +++ b/image/encoders/webp/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SOURCES += [ + "nsWebPEncoder.cpp", +] + +LOCAL_INCLUDES += [ + "/image", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/webp/nsWebPEncoder.cpp b/image/encoders/webp/nsWebPEncoder.cpp new file mode 100644 index 0000000000..72ac1388e9 --- /dev/null +++ b/image/encoders/webp/nsWebPEncoder.cpp @@ -0,0 +1,342 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// #include "ImageLogging.h" +#include "nsCRT.h" +#include "nsWebPEncoder.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "prprf.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/UniquePtrExtensions.h" + +using namespace mozilla; + +// static LazyLogModule sWEBPEncoderLog("WEBPEncoder"); + +NS_IMPL_ISUPPORTS(nsWebPEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +nsWebPEncoder::nsWebPEncoder() + : mFinished(false), + mImageBuffer(nullptr), + mImageBufferSize(0), + mImageBufferUsed(0), + mImageBufferReadPoint(0), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0), + mReentrantMonitor("nsWebPEncoder.mReentrantMonitor") {} + +nsWebPEncoder::~nsWebPEncoder() { + if (mImageBuffer) { + WebPFree(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } +} + +// nsWebPEncoder::InitFromData +// +// One output option is supported: "quality=X" where X is an integer in the +// range 0-100. Higher values for X give better quality. + +NS_IMETHODIMP +nsWebPEncoder::InitFromData(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + NS_ENSURE_ARG(aData); + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // Stride is the padded width of each row, so it better be longer (I'm afraid + // people will not understand what stride means, so check it well) + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData"); + return NS_ERROR_INVALID_ARG; + } + + // can't initialize more than once + if (mImageBuffer != nullptr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // options: we only have one option so this is easy + int quality = 92; + if (aOutputOptions.Length() > 0) { + // have options string + const nsString qualityPrefix(u"quality="_ns); + if (aOutputOptions.Length() > qualityPrefix.Length() && + StringBeginsWith(aOutputOptions, qualityPrefix)) { + // have quality string + nsCString value = NS_ConvertUTF16toUTF8( + Substring(aOutputOptions, qualityPrefix.Length())); + int newquality = -1; + if (PR_sscanf(value.get(), "%d", &newquality) == 1) { + if (newquality >= 0 && newquality <= 100) { + quality = newquality; + } else { + NS_WARNING( + "Quality value out of range, should be 0-100," + " using default"); + } + } else { + NS_WARNING( + "Quality value invalid, should be integer 0-100," + " using default"); + } + } else { + return NS_ERROR_INVALID_ARG; + } + } + + size_t size = 0; + + CheckedInt32 width = CheckedInt32(aWidth); + CheckedInt32 height = CheckedInt32(aHeight); + CheckedInt32 stride = CheckedInt32(aStride); + if (!width.isValid() || !height.isValid() || !stride.isValid() || + !(CheckedUint32(aStride) * CheckedUint32(aHeight)).isValid()) { + return NS_ERROR_INVALID_ARG; + } + + if (aInputFormat == INPUT_FORMAT_RGB) { + size = quality == 100 + ? WebPEncodeLosslessRGB(aData, width.value(), height.value(), + stride.value(), &mImageBuffer) + : WebPEncodeRGB(aData, width.value(), height.value(), + stride.value(), quality, &mImageBuffer); + } else if (aInputFormat == INPUT_FORMAT_RGBA) { + size = quality == 100 + ? WebPEncodeLosslessRGBA(aData, width.value(), height.value(), + stride.value(), &mImageBuffer) + : WebPEncodeRGBA(aData, width.value(), height.value(), + stride.value(), quality, &mImageBuffer); + } else if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + UniquePtr aDest = + MakeUniqueFallible(aStride * aHeight); + if (NS_WARN_IF(!aDest)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t y = 0; y < aHeight; y++) { + for (uint32_t x = 0; x < aWidth; x++) { + const uint32_t& pixelIn = + ((const uint32_t*)(aData))[y * aStride / 4 + x]; + uint8_t* pixelOut = &aDest[y * aStride + x * 4]; + + uint8_t alpha = (pixelIn & 0xff000000) >> 24; + pixelOut[3] = alpha; + if (alpha == 255) { + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff); + } else if (alpha == 0) { + pixelOut[0] = pixelOut[1] = pixelOut[2] = 0; + } else { + pixelOut[0] = + (((pixelIn & 0xff0000) >> 16) * 255 + alpha / 2) / alpha; + pixelOut[1] = (((pixelIn & 0x00ff00) >> 8) * 255 + alpha / 2) / alpha; + pixelOut[2] = (((pixelIn & 0x0000ff)) * 255 + alpha / 2) / alpha; + } + } + } + + size = + quality == 100 + ? WebPEncodeLosslessRGBA(aDest.get(), width.value(), height.value(), + stride.value(), &mImageBuffer) + : WebPEncodeRGBA(aDest.get(), width.value(), height.value(), + stride.value(), quality, &mImageBuffer); + } + + mFinished = true; + + if (size == 0) { + return NS_ERROR_FAILURE; + } + + mImageBufferUsed = size; + + return NS_OK; +} + +// nsWebPEncoder::StartImageEncode +// +// +// See ::InitFromData for other info. +NS_IMETHODIMP +nsWebPEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// Returns the number of bytes in the image buffer used. +NS_IMETHODIMP +nsWebPEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferUsed; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsWebPEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast(mImageBuffer); + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::AddImageFrame(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, + uint32_t aStride, uint32_t aInputFormat, + const nsAString& aFrameOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsWebPEncoder::EndImageEncode() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsWebPEncoder::Close() { + if (mImageBuffer) { + WebPFree(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::Available(uint64_t* _retval) { + if (!mImageBuffer) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mImageBufferUsed - mImageBufferReadPoint; + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::StreamStatus() { + return mImageBuffer ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +NS_IMETHODIMP +nsWebPEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +nsWebPEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + // Avoid another thread reallocing the buffer underneath us + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + uint32_t maxCount = mImageBufferUsed - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast(mImageBuffer + mImageBufferReadPoint), 0, + aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // 1 KB seems good. We don't want to + // notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the + // main thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsWebPEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +void nsWebPEncoder::NotifyListener() { + // We might call this function on multiple threads (any threads that call + // AsyncWait and any that do encoding) so we lock to avoid notifying the + // listener twice about the same data (which generally leads to a truncated + // image). + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + if (mCallback && + (mImageBufferUsed - mImageBufferReadPoint >= mNotifyThreshold || + mFinished)) { + nsCOMPtr callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsWebPEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could reenter + // AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} diff --git a/image/encoders/webp/nsWebPEncoder.h b/image/encoders/webp/nsWebPEncoder.h new file mode 100644 index 0000000000..8628d12388 --- /dev/null +++ b/image/encoders/webp/nsWebPEncoder.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_encoders_webp_nsWebPEncoder_h +#define mozilla_image_encoders_webp_nsWebPEncoder_h + +#include "webp/encode.h" + +#include "imgIEncoder.h" +#include "nsCOMPtr.h" + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" + +#define NS_WEBPENCODER_CID \ + { /* a8e5a8e5-bebf-4512-9f50-e41e4748ce28 */ \ + 0xa8e5a8e5, 0xbebf, 0x4512, { \ + 0x9f, 0x50, 0xe4, 0x1e, 0x47, 0x48, 0xce, 0x28 \ + } \ + } + +// Provides WEBP encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsWebPEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsWebPEncoder(); + + protected: + ~nsWebPEncoder(); + + void NotifyListener(); + + bool mFinished; + + // image buffer + uint8_t* mImageBuffer; + uint32_t mImageBufferSize; + uint32_t mImageBufferUsed; + + uint32_t mImageBufferReadPoint; + + nsCOMPtr mCallback; + nsCOMPtr mCallbackTarget; + uint32_t mNotifyThreshold; + + // nsWebPEncoder is designed to allow one thread to pump data into it while + // another reads from it. We lock to ensure that the buffer remains + // append-only while we read from it (that it is not realloced) and to + // ensure that only one thread dispatches a callback for each call to + // AsyncWait. + ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; +}; +#endif // mozilla_image_encoders_webp_nsWebPEncoder_h diff --git a/image/imgFrame.cpp b/image/imgFrame.cpp new file mode 100644 index 0000000000..0df40fcfa9 --- /dev/null +++ b/image/imgFrame.cpp @@ -0,0 +1,729 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "imgFrame.h" +#include "ImageRegion.h" +#include "SurfaceCache.h" + +#include "prenv.h" + +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxPlatform.h" + +#include "gfxUtils.h" + +#include "MainThreadUtils.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_browser.h" +#include "nsMargin.h" +#include "nsRefreshDriver.h" +#include "nsThreadUtils.h" + +#include // for min, max + +namespace mozilla { + +using namespace gfx; + +namespace image { + +/** + * This class is identical to SourceSurfaceSharedData but returns a different + * type so that SharedSurfacesChild is aware imagelib wants to recycle this + * surface for future animation frames. + */ +class RecyclingSourceSurfaceSharedData final : public SourceSurfaceSharedData { + public: + MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(RecyclingSourceSurfaceSharedData, + override) + + SurfaceType GetType() const override { + return SurfaceType::DATA_RECYCLING_SHARED; + } +}; + +static already_AddRefed AllocateBufferForImage( + const IntSize& size, SurfaceFormat format, bool aShouldRecycle = false) { + // Stride must be a multiple of four or cairo will complain. + int32_t stride = (size.width * BytesPerPixel(format) + 0x3) & ~0x3; + + RefPtr newSurf; + if (aShouldRecycle) { + newSurf = new RecyclingSourceSurfaceSharedData(); + } else { + newSurf = new SourceSurfaceSharedData(); + } + if (!newSurf->Init(size, stride, format)) { + return nullptr; + } + return newSurf.forget(); +} + +static bool GreenSurface(SourceSurfaceSharedData* aSurface, + const IntSize& aSize, SurfaceFormat aFormat) { + int32_t stride = aSurface->Stride(); + uint32_t* surfaceData = reinterpret_cast(aSurface->GetData()); + uint32_t surfaceDataLength = (stride * aSize.height) / sizeof(uint32_t); + + // Start by assuming that GG is in the second byte and + // AA is in the final byte -- the most common case. + uint32_t color = mozilla::NativeEndian::swapFromBigEndian(0x00FF00FF); + + // We are only going to handle this type of test under + // certain circumstances. + MOZ_ASSERT(surfaceData); + MOZ_ASSERT(aFormat == SurfaceFormat::B8G8R8A8 || + aFormat == SurfaceFormat::B8G8R8X8 || + aFormat == SurfaceFormat::R8G8B8A8 || + aFormat == SurfaceFormat::R8G8B8X8 || + aFormat == SurfaceFormat::A8R8G8B8 || + aFormat == SurfaceFormat::X8R8G8B8); + MOZ_ASSERT((stride * aSize.height) % sizeof(uint32_t)); + + if (aFormat == SurfaceFormat::A8R8G8B8 || + aFormat == SurfaceFormat::X8R8G8B8) { + color = mozilla::NativeEndian::swapFromBigEndian(0xFF00FF00); + } + + for (uint32_t i = 0; i < surfaceDataLength; i++) { + surfaceData[i] = color; + } + + return true; +} + +static bool ClearSurface(SourceSurfaceSharedData* aSurface, + const IntSize& aSize, SurfaceFormat aFormat) { + int32_t stride = aSurface->Stride(); + uint8_t* data = aSurface->GetData(); + MOZ_ASSERT(data); + + if (aFormat == SurfaceFormat::OS_RGBX) { + // Skia doesn't support RGBX surfaces, so ensure the alpha value is set + // to opaque white. While it would be nice to only do this for Skia, + // imgFrame can run off main thread and past shutdown where + // we might not have gfxPlatform, so just memset every time instead. + memset(data, 0xFF, stride * aSize.height); + } else if (aSurface->OnHeap()) { + // We only need to memset it if the buffer was allocated on the heap. + // Otherwise, it's allocated via mmap and refers to a zeroed page and will + // be COW once it's written to. + memset(data, 0, stride * aSize.height); + } + + return true; +} + +imgFrame::imgFrame() + : mMonitor("imgFrame"), + mDecoded(0, 0, 0, 0), + mAborted(false), + mFinished(false), + mShouldRecycle(false), + mTimeout(FrameTimeout::FromRawMilliseconds(100)), + mDisposalMethod(DisposalMethod::NOT_SPECIFIED), + mBlendMethod(BlendMethod::OVER), + mFormat(SurfaceFormat::UNKNOWN), + mNonPremult(false) {} + +imgFrame::~imgFrame() { +#ifdef DEBUG + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mAborted || AreAllPixelsWritten()); + MOZ_ASSERT(mAborted || mFinished); +#endif +} + +nsresult imgFrame::InitForDecoder(const nsIntSize& aImageSize, + SurfaceFormat aFormat, bool aNonPremult, + const Maybe& aAnimParams, + bool aShouldRecycle) { + // Assert for properties that should be verified by decoders, + // warn for properties related to bad content. + if (!SurfaceCache::IsLegalSize(aImageSize)) { + NS_WARNING("Should have legal image size"); + MonitorAutoLock lock(mMonitor); + mAborted = true; + return NS_ERROR_FAILURE; + } + + mImageSize = aImageSize; + + // May be updated shortly after InitForDecoder by BlendAnimationFilter + // because it needs to take into consideration the previous frames to + // properly calculate. We start with the whole frame as dirty. + mDirtyRect = GetRect(); + + if (aAnimParams) { + mBlendRect = aAnimParams->mBlendRect; + mTimeout = aAnimParams->mTimeout; + mBlendMethod = aAnimParams->mBlendMethod; + mDisposalMethod = aAnimParams->mDisposalMethod; + } else { + mBlendRect = GetRect(); + } + + if (aShouldRecycle) { + // If we are recycling then we should always use BGRA for the underlying + // surface because if we use BGRX, the next frame composited into the + // surface could be BGRA and cause rendering problems. + MOZ_ASSERT(aAnimParams); + mFormat = SurfaceFormat::OS_RGBA; + } else { + mFormat = aFormat; + } + + mNonPremult = aNonPremult; + + MonitorAutoLock lock(mMonitor); + mShouldRecycle = aShouldRecycle; + + MOZ_ASSERT(!mRawSurface, "Called imgFrame::InitForDecoder() twice?"); + + mRawSurface = AllocateBufferForImage(mImageSize, mFormat, mShouldRecycle); + if (!mRawSurface) { + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + if (StaticPrefs::browser_measurement_render_anims_and_video_solid() && + aAnimParams) { + mBlankRawSurface = AllocateBufferForImage(mImageSize, mFormat); + if (!mBlankRawSurface) { + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + } + + if (!ClearSurface(mRawSurface, mImageSize, mFormat)) { + NS_WARNING("Could not clear allocated buffer"); + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mBlankRawSurface) { + if (!GreenSurface(mBlankRawSurface, mImageSize, mFormat)) { + NS_WARNING("Could not clear allocated blank buffer"); + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return NS_OK; +} + +nsresult imgFrame::InitForDecoderRecycle(const AnimationParams& aAnimParams) { + // We want to recycle this frame, but there is no guarantee that consumers are + // done with it in a timely manner. Let's ensure they are done with it first. + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(mRawSurface); + + if (!mShouldRecycle) { + // This frame either was never marked as recyclable, or the flag was cleared + // for a caller which does not support recycling. + return NS_ERROR_NOT_AVAILABLE; + } + + // Ensure we account for all internal references to the surface. + MozRefCountType internalRefs = 1; + if (mOptSurface == mRawSurface) { + ++internalRefs; + } + + if (mRawSurface->refCount() > internalRefs) { + if (NS_IsMainThread()) { + // We should never be both decoding and recycling on the main thread. Sync + // decoding can only be used to produce the first set of frames. Those + // either never use recycling because advancing was blocked (main thread + // is busy) or we were auto-advancing (to seek to a frame) and the frames + // were never accessed (and thus cannot have recycle locks). + MOZ_ASSERT_UNREACHABLE("Recycling/decoding on the main thread?"); + return NS_ERROR_NOT_AVAILABLE; + } + + // We don't want to wait forever to reclaim the frame because we have no + // idea why it is still held. It is possibly due to OMTP. Since we are off + // the main thread, and we generally have frames already buffered for the + // animation, we can afford to wait a short period of time to hopefully + // complete the transaction and reclaim the buffer. + // + // We choose to wait for, at most, the refresh driver interval, so that we + // won't skip more than one frame. If the frame is still in use due to + // outstanding transactions, we are already skipping frames. If the frame + // is still in use for some other purpose, it won't be returned to the pool + // and its owner can hold onto it forever without additional impact here. + int32_t refreshInterval = + std::max(std::min(nsRefreshDriver::DefaultInterval(), 20), 4); + TimeDuration waitInterval = + TimeDuration::FromMilliseconds(refreshInterval >> 2); + TimeStamp timeout = + TimeStamp::Now() + TimeDuration::FromMilliseconds(refreshInterval); + while (true) { + mMonitor.Wait(waitInterval); + if (mRawSurface->refCount() <= internalRefs) { + break; + } + + if (timeout <= TimeStamp::Now()) { + // We couldn't secure the frame for recycling. It will allocate a new + // frame instead. + return NS_ERROR_NOT_AVAILABLE; + } + } + } + + mBlendRect = aAnimParams.mBlendRect; + mTimeout = aAnimParams.mTimeout; + mBlendMethod = aAnimParams.mBlendMethod; + mDisposalMethod = aAnimParams.mDisposalMethod; + mDirtyRect = GetRect(); + + return NS_OK; +} + +nsresult imgFrame::InitWithDrawable(gfxDrawable* aDrawable, + const nsIntSize& aSize, + const SurfaceFormat aFormat, + SamplingFilter aSamplingFilter, + uint32_t aImageFlags, + gfx::BackendType aBackend) { + // Assert for properties that should be verified by decoders, + // warn for properties related to bad content. + if (!SurfaceCache::IsLegalSize(aSize)) { + NS_WARNING("Should have legal image size"); + MonitorAutoLock lock(mMonitor); + mAborted = true; + return NS_ERROR_FAILURE; + } + + mImageSize = aSize; + mFormat = aFormat; + + RefPtr target; + + bool canUseDataSurface = Factory::DoesBackendSupportDataDrawtarget(aBackend); + if (canUseDataSurface) { + MonitorAutoLock lock(mMonitor); + // It's safe to use data surfaces for content on this platform, so we can + // get away with using volatile buffers. + MOZ_ASSERT(!mRawSurface, "Called imgFrame::InitWithDrawable() twice?"); + + mRawSurface = AllocateBufferForImage(mImageSize, mFormat); + if (!mRawSurface) { + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!ClearSurface(mRawSurface, mImageSize, mFormat)) { + NS_WARNING("Could not clear allocated buffer"); + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + target = gfxPlatform::CreateDrawTargetForData( + mRawSurface->GetData(), mImageSize, mRawSurface->Stride(), mFormat); + } else { + // We can't use data surfaces for content, so we'll create an offscreen + // surface instead. This means if someone later calls RawAccessRef(), we + // may have to do an expensive readback, but we warned callers about that in + // the documentation for this method. +#ifdef DEBUG + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mOptSurface, "Called imgFrame::InitWithDrawable() twice?"); + } +#endif + + if (gfxPlatform::GetPlatform()->SupportsAzureContentForType(aBackend)) { + target = gfxPlatform::GetPlatform()->CreateDrawTargetForBackend( + aBackend, mImageSize, mFormat); + } else { + target = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + mImageSize, mFormat); + } + } + + if (!target || !target->IsValid()) { + MonitorAutoLock lock(mMonitor); + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + // Draw using the drawable the caller provided. + gfxContext ctx(target); + + gfxUtils::DrawPixelSnapped(&ctx, aDrawable, SizeDouble(mImageSize), + ImageRegion::Create(ThebesRect(GetRect())), + mFormat, aSamplingFilter, aImageFlags); + + MonitorAutoLock lock(mMonitor); + if (canUseDataSurface && !mRawSurface) { + NS_WARNING("Failed to create SourceSurfaceSharedData"); + mAborted = true; + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!canUseDataSurface) { + // We used an offscreen surface, which is an "optimized" surface from + // imgFrame's perspective. + mOptSurface = target->Snapshot(); + } else { + FinalizeSurfaceInternal(); + } + + // If we reach this point, we should regard ourselves as complete. + mDecoded = GetRect(); + mFinished = true; + + MOZ_ASSERT(AreAllPixelsWritten()); + + return NS_OK; +} + +DrawableFrameRef imgFrame::DrawableRef() { return DrawableFrameRef(this); } + +RawAccessFrameRef imgFrame::RawAccessRef() { return RawAccessFrameRef(this); } + +imgFrame::SurfaceWithFormat imgFrame::SurfaceForDrawing( + bool aDoPartialDecode, bool aDoTile, ImageRegion& aRegion, + SourceSurface* aSurface) { + MOZ_ASSERT(NS_IsMainThread()); + mMonitor.AssertCurrentThreadOwns(); + + if (!aDoPartialDecode) { + return SurfaceWithFormat(new gfxSurfaceDrawable(aSurface, mImageSize), + mFormat); + } + + gfxRect available = + gfxRect(mDecoded.X(), mDecoded.Y(), mDecoded.Width(), mDecoded.Height()); + + if (aDoTile) { + // Create a temporary surface. + // Give this surface an alpha channel because there are + // transparent pixels in the padding or undecoded area + RefPtr target = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + mImageSize, SurfaceFormat::OS_RGBA); + if (!target) { + return SurfaceWithFormat(); + } + + SurfacePattern pattern(aSurface, aRegion.GetExtendMode(), + Matrix::Translation(mDecoded.X(), mDecoded.Y())); + target->FillRect(ToRect(aRegion.Intersect(available).Rect()), pattern); + + RefPtr newsurf = target->Snapshot(); + return SurfaceWithFormat(new gfxSurfaceDrawable(newsurf, mImageSize), + target->GetFormat()); + } + + // Not tiling, and we have a surface, so we can account for + // a partial decode just by twiddling parameters. + aRegion = aRegion.Intersect(available); + IntSize availableSize(mDecoded.Width(), mDecoded.Height()); + + return SurfaceWithFormat(new gfxSurfaceDrawable(aSurface, availableSize), + mFormat); +} + +bool imgFrame::Draw(gfxContext* aContext, const ImageRegion& aRegion, + SamplingFilter aSamplingFilter, uint32_t aImageFlags, + float aOpacity) { + AUTO_PROFILER_LABEL("imgFrame::Draw", GRAPHICS); + + MOZ_ASSERT(NS_IsMainThread()); + NS_ASSERTION(!aRegion.Rect().IsEmpty(), "Drawing empty region!"); + NS_ASSERTION(!aRegion.IsRestricted() || + !aRegion.Rect().Intersect(aRegion.Restriction()).IsEmpty(), + "We must be allowed to sample *some* source pixels!"); + + // Perform the draw and freeing of the surface outside the lock. We want to + // avoid contention with the decoder if we can. The surface may also attempt + // to relock the monitor if it is freed (e.g. RecyclingSourceSurface). + RefPtr surf; + SurfaceWithFormat surfaceResult; + ImageRegion region(aRegion); + gfxRect imageRect(0, 0, mImageSize.width, mImageSize.height); + + { + MonitorAutoLock lock(mMonitor); + + bool doPartialDecode = !AreAllPixelsWritten(); + + // Most draw targets will just use the surface only during DrawPixelSnapped + // but captures/recordings will retain a reference outside this stack + // context. While in theory a decoder thread could be trying to recycle this + // frame at this very moment, in practice the only way we can get here is if + // this frame is the current frame of the animation. Since we can only + // advance on the main thread, we know nothing else will try to use it. + DrawTarget* drawTarget = aContext->GetDrawTarget(); + bool recording = drawTarget->GetBackendType() == BackendType::RECORDING; + RefPtr surf = GetSourceSurfaceInternal(); + if (!surf) { + return false; + } + + bool doTile = !imageRect.Contains(aRegion.Rect()) && + !(aImageFlags & imgIContainer::FLAG_CLAMP); + + surfaceResult = SurfaceForDrawing(doPartialDecode, doTile, region, surf); + + // If we are recording, then we cannot recycle the surface. The blob + // rasterizer is not properly synchronized for recycling in the compositor + // process. The easiest thing to do is just mark the frames it consumes as + // non-recyclable. + if (recording && surfaceResult.IsValid()) { + mShouldRecycle = false; + } + } + + if (surfaceResult.IsValid()) { + gfxUtils::DrawPixelSnapped(aContext, surfaceResult.mDrawable, + imageRect.Size(), region, surfaceResult.mFormat, + aSamplingFilter, aImageFlags, aOpacity); + } + + return true; +} + +nsresult imgFrame::ImageUpdated(const nsIntRect& aUpdateRect) { + MonitorAutoLock lock(mMonitor); + return ImageUpdatedInternal(aUpdateRect); +} + +nsresult imgFrame::ImageUpdatedInternal(const nsIntRect& aUpdateRect) { + mMonitor.AssertCurrentThreadOwns(); + + // Clamp to the frame rect to ensure that decoder bugs don't result in a + // decoded rect that extends outside the bounds of the frame rect. + IntRect updateRect = aUpdateRect.Intersect(GetRect()); + if (updateRect.IsEmpty()) { + return NS_OK; + } + + mDecoded.UnionRect(mDecoded, updateRect); + + // Update our invalidation counters for any consumers watching for changes + // in the surface. + if (mRawSurface) { + mRawSurface->Invalidate(updateRect); + } + return NS_OK; +} + +void imgFrame::Finish(Opacity aFrameOpacity /* = Opacity::SOME_TRANSPARENCY */, + bool aFinalize /* = true */, + bool aOrientationSwapsWidthAndHeight /* = false */) { + MonitorAutoLock lock(mMonitor); + + IntRect frameRect(GetRect()); + if (!mDecoded.IsEqualEdges(frameRect)) { + // The decoder should have produced rows starting from either the bottom or + // the top of the image. We need to calculate the region for which we have + // not yet invalidated. And if the orientation swaps width and height then + // its from the left or right. + IntRect delta(0, 0, frameRect.width, 0); + if (!aOrientationSwapsWidthAndHeight) { + delta.width = frameRect.width; + if (mDecoded.y == 0) { + delta.y = mDecoded.height; + delta.height = frameRect.height - mDecoded.height; + } else if (mDecoded.y + mDecoded.height == frameRect.height) { + delta.height = frameRect.height - mDecoded.y; + } else { + MOZ_ASSERT_UNREACHABLE("Decoder only updated middle of image!"); + delta = frameRect; + } + } else { + delta.height = frameRect.height; + if (mDecoded.x == 0) { + delta.x = mDecoded.width; + delta.width = frameRect.width - mDecoded.width; + } else if (mDecoded.x + mDecoded.width == frameRect.width) { + delta.width = frameRect.width - mDecoded.x; + } else { + MOZ_ASSERT_UNREACHABLE("Decoder only updated middle of image!"); + delta = frameRect; + } + } + + ImageUpdatedInternal(delta); + } + + MOZ_ASSERT(mDecoded.IsEqualEdges(frameRect)); + + if (aFinalize) { + FinalizeSurfaceInternal(); + } + + mFinished = true; + + // The image is now complete, wake up anyone who's waiting. + mMonitor.NotifyAll(); +} + +uint32_t imgFrame::GetImageBytesPerRow() const { + mMonitor.AssertCurrentThreadOwns(); + + if (mRawSurface) { + return mImageSize.width * BytesPerPixel(mFormat); + } + + return 0; +} + +uint32_t imgFrame::GetImageDataLength() const { + return GetImageBytesPerRow() * mImageSize.height; +} + +void imgFrame::GetImageData(uint8_t** aData, uint32_t* aLength) const { + MonitorAutoLock lock(mMonitor); + GetImageDataInternal(aData, aLength); +} + +void imgFrame::GetImageDataInternal(uint8_t** aData, uint32_t* aLength) const { + mMonitor.AssertCurrentThreadOwns(); + MOZ_ASSERT(mRawSurface); + + if (mRawSurface) { + // TODO: This is okay for now because we only realloc shared surfaces on + // the main thread after decoding has finished, but if animations want to + // read frame data off the main thread, we will need to reconsider this. + *aData = mRawSurface->GetData(); + MOZ_ASSERT(*aData, + "mRawSurface is non-null, but GetData is null in GetImageData"); + } else { + *aData = nullptr; + } + + *aLength = GetImageDataLength(); +} + +uint8_t* imgFrame::GetImageData() const { + uint8_t* data; + uint32_t length; + GetImageData(&data, &length); + return data; +} + +void imgFrame::FinalizeSurface() { + MonitorAutoLock lock(mMonitor); + FinalizeSurfaceInternal(); +} + +void imgFrame::FinalizeSurfaceInternal() { + mMonitor.AssertCurrentThreadOwns(); + + // Not all images will have mRawSurface to finalize (i.e. paletted images). + if (mShouldRecycle || !mRawSurface || + mRawSurface->GetType() != SurfaceType::DATA_SHARED) { + return; + } + + auto* sharedSurf = static_cast(mRawSurface.get()); + sharedSurf->Finalize(); +} + +already_AddRefed imgFrame::GetSourceSurface() { + MonitorAutoLock lock(mMonitor); + return GetSourceSurfaceInternal(); +} + +already_AddRefed imgFrame::GetSourceSurfaceInternal() { + mMonitor.AssertCurrentThreadOwns(); + + if (mOptSurface) { + if (mOptSurface->IsValid()) { + RefPtr surf(mOptSurface); + return surf.forget(); + } + mOptSurface = nullptr; + } + + if (mBlankRawSurface) { + // We are going to return the blank surface because of the flags. + // We are including comments here that are copied from below + // just so that we are on the same page! + RefPtr surf(mBlankRawSurface); + return surf.forget(); + } + + RefPtr surf(mRawSurface); + return surf.forget(); +} + +void imgFrame::Abort() { + MonitorAutoLock lock(mMonitor); + + mAborted = true; + + // Wake up anyone who's waiting. + mMonitor.NotifyAll(); +} + +bool imgFrame::IsAborted() const { + MonitorAutoLock lock(mMonitor); + return mAborted; +} + +bool imgFrame::IsFinished() const { + MonitorAutoLock lock(mMonitor); + return mFinished; +} + +void imgFrame::WaitUntilFinished() const { + MonitorAutoLock lock(mMonitor); + + while (true) { + // Return if we're aborted or complete. + if (mAborted || mFinished) { + return; + } + + // Not complete yet, so we'll have to wait. + mMonitor.Wait(); + } +} + +bool imgFrame::AreAllPixelsWritten() const { + mMonitor.AssertCurrentThreadOwns(); + return mDecoded.IsEqualInterior(GetRect()); +} + +void imgFrame::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) const { + MonitorAutoLock lock(mMonitor); + + AddSizeOfCbData metadata; + metadata.mFinished = mFinished; + + if (mOptSurface) { + metadata.mHeapBytes += aMallocSizeOf(mOptSurface); + + SourceSurface::SizeOfInfo info; + mOptSurface->SizeOfExcludingThis(aMallocSizeOf, info); + metadata.Accumulate(info); + } + if (mRawSurface) { + metadata.mHeapBytes += aMallocSizeOf(mRawSurface); + + SourceSurface::SizeOfInfo info; + mRawSurface->SizeOfExcludingThis(aMallocSizeOf, info); + metadata.Accumulate(info); + } + + aCallback(metadata); +} + +} // namespace image +} // namespace mozilla diff --git a/image/imgFrame.h b/image/imgFrame.h new file mode 100644 index 0000000000..c0049c36ca --- /dev/null +++ b/image/imgFrame.h @@ -0,0 +1,422 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_imgFrame_h +#define mozilla_image_imgFrame_h + +#include +#include + +#include "AnimationParams.h" +#include "MainThreadUtils.h" +#include "gfxDrawable.h" +#include "mozilla/layers/SourceSurfaceSharedData.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Monitor.h" +#include "nsRect.h" + +namespace mozilla { +namespace image { + +class ImageRegion; +class DrawableFrameRef; +class RawAccessFrameRef; + +enum class Opacity : uint8_t { FULLY_OPAQUE, SOME_TRANSPARENCY }; + +class imgFrame { + typedef gfx::SourceSurfaceSharedData SourceSurfaceSharedData; + typedef gfx::DrawTarget DrawTarget; + typedef gfx::SamplingFilter SamplingFilter; + typedef gfx::IntPoint IntPoint; + typedef gfx::IntRect IntRect; + typedef gfx::IntSize IntSize; + typedef gfx::SourceSurface SourceSurface; + typedef gfx::SurfaceFormat SurfaceFormat; + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(imgFrame) + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(imgFrame) + + imgFrame(); + + /** + * Initialize this imgFrame with an empty surface and prepare it for being + * written to by a decoder. + * + * This is appropriate for use with decoded images, but it should not be used + * when drawing content into an imgFrame, as it may use a different graphics + * backend than normal content drawing. + */ + nsresult InitForDecoder(const nsIntSize& aImageSize, SurfaceFormat aFormat, + bool aNonPremult, + const Maybe& aAnimParams, + bool aShouldRecycle); + + /** + * Reinitialize this imgFrame with the new parameters, but otherwise retain + * the underlying buffer. + * + * This is appropriate for use with animated images, where the decoder was + * given an IDecoderFrameRecycler object which may yield a recycled imgFrame + * that was discarded to save memory. + */ + nsresult InitForDecoderRecycle(const AnimationParams& aAnimParams); + + /** + * Initialize this imgFrame with a new surface and draw the provided + * gfxDrawable into it. + * + * This is appropriate to use when drawing content into an imgFrame, as it + * uses the same graphics backend as normal content drawing. The downside is + * that the underlying surface may not be stored in a volatile buffer on all + * platforms, and raw access to the surface (using RawAccessRef()) may be much + * more expensive than in the InitForDecoder() case. + * + * aBackend specifies the DrawTarget backend type this imgFrame is supposed + * to be drawn to. + */ + nsresult InitWithDrawable(gfxDrawable* aDrawable, const nsIntSize& aSize, + const SurfaceFormat aFormat, + SamplingFilter aSamplingFilter, + uint32_t aImageFlags, gfx::BackendType aBackend); + + DrawableFrameRef DrawableRef(); + + /** + * Create a RawAccessFrameRef for the frame. + */ + RawAccessFrameRef RawAccessRef(); + + bool Draw(gfxContext* aContext, const ImageRegion& aRegion, + SamplingFilter aSamplingFilter, uint32_t aImageFlags, + float aOpacity); + + nsresult ImageUpdated(const nsIntRect& aUpdateRect); + + /** + * Mark this imgFrame as completely decoded, and set final options. + * + * You must always call either Finish() or Abort() before releasing the last + * RawAccessFrameRef pointing to an imgFrame. + * + * @param aFrameOpacity Whether this imgFrame is opaque. + * @param aFinalize Finalize the underlying surface (e.g. so that it + * may be marked as read only if possible). + */ + void Finish(Opacity aFrameOpacity = Opacity::SOME_TRANSPARENCY, + bool aFinalize = true, + bool aOrientationSwapsWidthAndHeight = false); + + /** + * Mark this imgFrame as aborted. This informs the imgFrame that if it isn't + * completely decoded now, it never will be. + * + * You must always call either Finish() or Abort() before releasing the last + * RawAccessFrameRef pointing to an imgFrame. + */ + void Abort(); + + /** + * Returns true if this imgFrame has been aborted. + */ + bool IsAborted() const; + + /** + * Returns true if this imgFrame is completely decoded. + */ + bool IsFinished() const; + + /** + * Blocks until this imgFrame is either completely decoded, or is marked as + * aborted. + * + * Note that calling this on the main thread _blocks the main thread_. Be very + * careful in your use of this method to avoid excessive main thread jank or + * deadlock. + */ + void WaitUntilFinished() const; + + /** + * Returns the number of bytes per pixel this imgFrame requires. + */ + uint32_t GetBytesPerPixel() const { return 4; } + + const IntSize& GetSize() const { return mImageSize; } + IntRect GetRect() const { return IntRect(IntPoint(0, 0), mImageSize); } + const IntRect& GetBlendRect() const { return mBlendRect; } + IntRect GetBoundedBlendRect() const { + return mBlendRect.Intersect(GetRect()); + } + nsIntRect GetDecodedRect() const { + MonitorAutoLock lock(mMonitor); + return mDecoded; + } + FrameTimeout GetTimeout() const { return mTimeout; } + BlendMethod GetBlendMethod() const { return mBlendMethod; } + DisposalMethod GetDisposalMethod() const { return mDisposalMethod; } + bool FormatHasAlpha() const { return mFormat == SurfaceFormat::OS_RGBA; } + void GetImageData(uint8_t** aData, uint32_t* length) const; + uint8_t* GetImageData() const; + + const IntRect& GetDirtyRect() const { return mDirtyRect; } + void SetDirtyRect(const IntRect& aDirtyRect) { mDirtyRect = aDirtyRect; } + + void FinalizeSurface(); + already_AddRefed GetSourceSurface(); + + struct AddSizeOfCbData : public SourceSurface::SizeOfInfo { + AddSizeOfCbData() + : SourceSurface::SizeOfInfo(), mIndex(0), mFinished(false) {} + + size_t mIndex; + bool mFinished; + }; + + typedef std::function AddSizeOfCb; + + void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + const AddSizeOfCb& aCallback) const; + + private: // methods + ~imgFrame(); + + bool AreAllPixelsWritten() const MOZ_REQUIRES(mMonitor); + nsresult ImageUpdatedInternal(const nsIntRect& aUpdateRect); + void GetImageDataInternal(uint8_t** aData, uint32_t* length) const; + uint32_t GetImageBytesPerRow() const; + uint32_t GetImageDataLength() const; + void FinalizeSurfaceInternal(); + already_AddRefed GetSourceSurfaceInternal(); + + struct SurfaceWithFormat { + RefPtr mDrawable; + SurfaceFormat mFormat; + SurfaceWithFormat() : mFormat(SurfaceFormat::UNKNOWN) {} + SurfaceWithFormat(gfxDrawable* aDrawable, SurfaceFormat aFormat) + : mDrawable(aDrawable), mFormat(aFormat) {} + SurfaceWithFormat(SurfaceWithFormat&& aOther) + : mDrawable(std::move(aOther.mDrawable)), mFormat(aOther.mFormat) {} + SurfaceWithFormat& operator=(SurfaceWithFormat&& aOther) { + mDrawable = std::move(aOther.mDrawable); + mFormat = aOther.mFormat; + return *this; + } + SurfaceWithFormat& operator=(const SurfaceWithFormat& aOther) = delete; + SurfaceWithFormat(const SurfaceWithFormat& aOther) = delete; + bool IsValid() { return !!mDrawable; } + }; + + SurfaceWithFormat SurfaceForDrawing(bool aDoPartialDecode, bool aDoTile, + ImageRegion& aRegion, + SourceSurface* aSurface); + + private: // data + friend class DrawableFrameRef; + friend class RawAccessFrameRef; + friend class UnlockImageDataRunnable; + + ////////////////////////////////////////////////////////////////////////////// + // Thread-safe mutable data, protected by mMonitor. + ////////////////////////////////////////////////////////////////////////////// + + mutable Monitor mMonitor; + + /** + * Used for rasterized images, this contains the raw pixel data. + */ + RefPtr mRawSurface MOZ_GUARDED_BY(mMonitor); + RefPtr mBlankRawSurface MOZ_GUARDED_BY(mMonitor); + + /** + * Used for vector images that were not rasterized directly. This might be a + * blob recording or native surface. + */ + RefPtr mOptSurface MOZ_GUARDED_BY(mMonitor); + + nsIntRect mDecoded MOZ_GUARDED_BY(mMonitor); + + bool mAborted MOZ_GUARDED_BY(mMonitor); + bool mFinished MOZ_GUARDED_BY(mMonitor); + bool mShouldRecycle MOZ_GUARDED_BY(mMonitor); + + ////////////////////////////////////////////////////////////////////////////// + // Effectively const data, only mutated in the Init methods. + ////////////////////////////////////////////////////////////////////////////// + + //! The size of the buffer we are decoding to. + IntSize mImageSize; + + //! The contents for the frame, as represented in the encoded image. This may + //! differ from mImageSize because it may be a partial frame. For the first + //! frame, this means we need to shift the data in place, and for animated + //! frames, it likely need to combine with a previous frame to get the full + //! contents. + IntRect mBlendRect; + + //! This is the region that has changed between this frame and the previous + //! frame of an animation. For the first frame, this will be the same as + //! mFrameRect. + IntRect mDirtyRect; + + //! The timeout for this frame. + FrameTimeout mTimeout; + + DisposalMethod mDisposalMethod; + BlendMethod mBlendMethod; + SurfaceFormat mFormat; + + bool mNonPremult; +}; + +/** + * A reference to an imgFrame that holds the imgFrame's surface in memory, + * allowing drawing. If you have a DrawableFrameRef |ref| and |if (ref)| returns + * true, then calls to Draw() and GetSourceSurface() are guaranteed to succeed. + */ +class DrawableFrameRef final { + typedef gfx::DataSourceSurface DataSourceSurface; + + public: + DrawableFrameRef() {} + + explicit DrawableFrameRef(imgFrame* aFrame) : mFrame(aFrame) { + MOZ_ASSERT(aFrame); + MonitorAutoLock lock(aFrame->mMonitor); + + if (aFrame->mRawSurface) { + mRef.emplace(aFrame->mRawSurface, DataSourceSurface::READ); + if (!mRef->IsMapped()) { + mFrame = nullptr; + mRef.reset(); + } + } else if (!aFrame->mOptSurface || !aFrame->mOptSurface->IsValid()) { + // The optimized surface has become invalid, so we need to redecode. + // For example, on Windows, there may have been a device reset, and + // all D2D surfaces now need to be recreated. + mFrame = nullptr; + } + } + + DrawableFrameRef(DrawableFrameRef&& aOther) + : mFrame(std::move(aOther.mFrame)), mRef(std::move(aOther.mRef)) {} + + DrawableFrameRef& operator=(DrawableFrameRef&& aOther) { + MOZ_ASSERT(this != &aOther, "Self-moves are prohibited"); + mFrame = std::move(aOther.mFrame); + mRef = std::move(aOther.mRef); + return *this; + } + + explicit operator bool() const { return bool(mFrame); } + + imgFrame* operator->() { + MOZ_ASSERT(mFrame); + return mFrame; + } + + const imgFrame* operator->() const { + MOZ_ASSERT(mFrame); + return mFrame; + } + + imgFrame* get() { return mFrame; } + const imgFrame* get() const { return mFrame; } + + void reset() { + mFrame = nullptr; + mRef.reset(); + } + + private: + DrawableFrameRef(const DrawableFrameRef& aOther) = delete; + DrawableFrameRef& operator=(const DrawableFrameRef& aOther) = delete; + + RefPtr mFrame; + Maybe mRef; +}; + +/** + * A reference to an imgFrame that holds the imgFrame's surface in memory in a + * format appropriate for access as raw data. If you have a RawAccessFrameRef + * |ref| and |if (ref)| is true, then calls to GetImageData() is guaranteed to + * succeed. This guarantee is stronger than DrawableFrameRef, so everything that + * a valid DrawableFrameRef guarantees is also guaranteed by a valid + * RawAccessFrameRef. + * + * This may be considerably more expensive than is necessary just for drawing, + * so only use this when you need to read or write the raw underlying image data + * that the imgFrame holds. + * + * Once all an imgFrame's RawAccessFrameRefs go out of scope, new + * RawAccessFrameRefs cannot be created. + */ +class RawAccessFrameRef final { + public: + RawAccessFrameRef() : mData(nullptr) {} + + explicit RawAccessFrameRef(imgFrame* aFrame) + : mFrame(aFrame), mData(nullptr) { + MOZ_ASSERT(mFrame, "Need a frame"); + + mData = mFrame->GetImageData(); + if (!mData) { + mFrame = nullptr; + } + } + + RawAccessFrameRef(RawAccessFrameRef&& aOther) + : mFrame(std::move(aOther.mFrame)), mData(aOther.mData) { + aOther.mData = nullptr; + } + + ~RawAccessFrameRef() {} + + RawAccessFrameRef& operator=(RawAccessFrameRef&& aOther) { + MOZ_ASSERT(this != &aOther, "Self-moves are prohibited"); + + mFrame = std::move(aOther.mFrame); + mData = aOther.mData; + aOther.mData = nullptr; + + return *this; + } + + explicit operator bool() const { return bool(mFrame); } + + imgFrame* operator->() { + MOZ_ASSERT(mFrame); + return mFrame.get(); + } + + const imgFrame* operator->() const { + MOZ_ASSERT(mFrame); + return mFrame; + } + + imgFrame* get() { return mFrame; } + const imgFrame* get() const { return mFrame; } + + void reset() { + mFrame = nullptr; + mData = nullptr; + } + + uint8_t* Data() const { return mData; } + + private: + RawAccessFrameRef(const RawAccessFrameRef& aOther) = delete; + RawAccessFrameRef& operator=(const RawAccessFrameRef& aOther) = delete; + + RefPtr mFrame; + uint8_t* mData; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_imgFrame_h diff --git a/image/imgICache.idl b/image/imgICache.idl new file mode 100644 index 0000000000..2368dfc961 --- /dev/null +++ b/image/imgICache.idl @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface imgIRequest; +interface nsIPrincipal; +interface nsIProperties; +interface nsIURI; + +webidl Document; + +%{ C++ +namespace mozilla { +class OriginAttributes; +} // mozilla namespace +%} + +[ptr] native OriginAttributesPtr(mozilla::OriginAttributes); + +/** + * imgICache interface + * + * @author Stuart Parmenter + * @version 0.1 + * @see imagelib2 + */ +[scriptable, builtinclass, uuid(bfdf23ff-378e-402e-8a6c-840f0c82b6c3)] +interface imgICache : nsISupports +{ + /** + * Evict images from the cache. + * + * @param chrome If TRUE, evict only chrome images. + * If FALSE, evict everything except chrome images. + */ + void clearCache(in boolean chrome); + + /** + * Evict images from the cache. + * + * @param uri The URI to remove. + * @param doc The document to remove the cache entry for. + * @throws NS_ERROR_NOT_AVAILABLE if \a uri was unable to be removed from + * the cache. + */ + [noscript] void removeEntry(in nsIURI uri, [optional] in Document doc); + + /** + * Evict images from the cache with the same origin and the same + * originAttributes of the passed principal, across all processes. May only be + * called from the parent process. + * + * @param aPrincipal The principal to clear cache entries for. The principals + * origin attributes are used to determine whether the private or normal + * browsing cache is cleared. + * @throws NS_ERROR_NOT_AVAILABLE if not called in the parent process. + */ + void removeEntriesFromPrincipalInAllProcesses(in nsIPrincipal aPrincipal); + + /** + * Evict images from the non-chrome cache which match the the given base + * domain. Matching cache entries will be cleared across all origin attributes + * and all processes. + * + * @param aBaseDomain base domain to delete cache entries for. + * @throws NS_ERROR_NOT_AVAILABLE if not called in the parent process. + */ + void removeEntriesFromBaseDomainInAllProcesses(in ACString aBaseDomain); + + /** + * Find Properties + * Used to get properties such as 'type' and 'content-disposition' + * 'type' is a nsISupportsCString containing the images' mime type such as + * 'image/png' + * 'content-disposition' will be a nsISupportsCString containing the header + * If you call this before any data has been loaded from a URI, it will + * succeed, but come back empty. + * + * Hopefully this will be removed with bug 805119 + * + * @param uri The URI to look up. + * @param doc Optional pointer to the document that the cache entry belongs to. + * @returns NULL if the URL was not found in the cache + */ + [must_use] + nsIProperties findEntryProperties(in nsIURI uri, + [optional] in Document doc); + + /** + * Make this cache instance respect private browsing notifications. This + * entails clearing the chrome and content caches whenever the + * last-pb-context-exited notification is observed. + */ + void respectPrivacyNotifications(); + + /** + * Clear the image cache for a document. Controlled documents are responsible + * for doing this manually when they get destroyed. + */ + [noscript, notxpcom] + void clearCacheForControlledDocument(in Document doc); +}; diff --git a/image/imgIContainer.idl b/image/imgIContainer.idl new file mode 100644 index 0000000000..abbc827878 --- /dev/null +++ b/image/imgIContainer.idl @@ -0,0 +1,692 @@ +/** -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl Document; + +%{C++ +#include "ImgDrawResult.h" +#include "gfxPoint.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/AspectRatio.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "nsRect.h" +#include "nsSize.h" +#include "nsTArray.h" +#include "limits.h" + +class gfxContext; + +namespace mozilla { +struct AspectRatio; + +namespace gfx { +class SourceSurface; +} + +class WindowRenderer; +namespace layers { +class ImageContainer; +} +} + +class nsIFrame; + +namespace mozilla { +class TimeStamp; +class SVGImageContext; +struct MediaFeatureChange; +} + +namespace mozilla { +namespace image { + +class ImageRegion; +class ImageIntRegion; +class WebRenderImageProvider; +struct Orientation; +struct Resolution; + +} +} + +%} + +native MaybeAspectRatio(mozilla::Maybe); +native ImgDrawResult(mozilla::image::ImgDrawResult); +[ptr] native gfxContext(gfxContext); +[ref] native gfxMatrix(gfxMatrix); +[ref] native gfxRect(gfxRect); +[ref] native gfxSize(gfxSize); +native SamplingFilter(mozilla::gfx::SamplingFilter); +[ref] native nsIntRect(nsIntRect); +native nsIntRectByVal(nsIntRect); +[ref] native nsIntSize(nsIntSize); +native nsSize(nsSize); +[ptr] native nsIFrame(nsIFrame); +native TempRefImageContainer(already_AddRefed); +[ptr] native ImageContainer(mozilla::layers::ImageContainer); +[ptr] native WebRenderImageProvider(mozilla::image::WebRenderImageProvider); +[ref] native ImageRegion(mozilla::image::ImageRegion); +[ptr] native WindowRenderer(mozilla::WindowRenderer); +native Orientation(mozilla::image::Orientation); +native ImageResolution(mozilla::image::Resolution); +[ref] native TimeStamp(mozilla::TimeStamp); +[ref] native SVGImageContext(mozilla::SVGImageContext); +[ref] native MaybeImageIntRegion(mozilla::Maybe); +native TempRefSourceSurface(already_AddRefed); +native TempRefImgIContainer(already_AddRefed); +native nsIntSizeByVal(nsIntSize); +[ref] native MediaFeatureChange(mozilla::MediaFeatureChange); + + +/** + * imgIContainer is the interface that represents an image. It allows + * access to frames as Thebes surfaces. It also allows drawing of images + * onto Thebes contexts. + * + * Internally, imgIContainer also manages animation of images. + */ +[scriptable, builtinclass, uuid(a8dbee24-ff86-4755-b40e-51175caf31af)] +interface imgIContainer : nsISupports +{ + /** + * The width of the container rectangle. In the case of any error, + * zero is returned, and an exception will be thrown. + */ + readonly attribute int32_t width; + + /** + * The height of the container rectangle. In the case of any error, + * zero is returned, and an exception will be thrown. + */ + readonly attribute int32_t height; + + /** + * The intrinsic size of this image in appunits. If the image has no intrinsic + * size in a dimension, -1 will be returned for that dimension. In the case of + * any error, an exception will be thrown. + */ + [noscript] readonly attribute nsSize intrinsicSize; + + /** + * The (dimensionless) intrinsic ratio of this image. In the case of any + * error, Nothing() will be returned. + */ + [notxpcom, nostdcall] readonly attribute MaybeAspectRatio intrinsicRatio; + + /** + * The x coordinate of the image's hotspot, or 0 if there is no hotspot. + */ + readonly attribute int32_t hotspotX; + + /** + * The y coordinate of the image's hotspot, or 0 if there is no hotspot. + */ + readonly attribute int32_t hotspotY; + + /** + * Given a size at which this image will be displayed, and the drawing + * parameters affecting how it will be drawn, returns the image size which + * should be used to draw to produce the highest quality result. This is the + * appropriate size, for example, to use as an input to the pixel snapping + * algorithm. + * + * For best results the size returned by this method should not be cached. It + * can change over time due to changes in the internal state of the image. + * + * @param aDest The size of the destination rect into which this image will be + * drawn, in device pixels. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + * @param aSamplingFilter The filter to be used if we're scaling the image. + * @param aFlags Flags of the FLAG_* variety + */ + [notxpcom, nostdcall] nsIntSizeByVal + optimalImageSizeForDest([const] in gfxSize aDest, in uint32_t aWhichFrame, + in SamplingFilter aSamplingFilter, in uint32_t aFlags); + + /** + * Enumerated values for the 'type' attribute (below). + */ + const unsigned short TYPE_RASTER = 0; + const unsigned short TYPE_VECTOR = 1; + const unsigned short TYPE_REQUEST = 2; + + /** + * The type of this image (one of the TYPE_* values above). + */ + [infallible] readonly attribute unsigned short type; + + /** + * Whether this image is animated. You can only be guaranteed that querying + * this will not throw if STATUS_DECODE_COMPLETE is set on the imgIRequest. + * + * @throws NS_ERROR_NOT_AVAILABLE if the animated state cannot be determined. + */ + readonly attribute boolean animated; + + /** + * Provider ID for image providers created by this image. + */ + [infallible] readonly attribute unsigned long providerId; + + /** + * Flags for imgIContainer operations. + * + * Meanings: + * + * FLAG_NONE: Lack of flags. + * + * FLAG_SYNC_DECODE: Forces synchronous/non-progressive decode of all + * available data before the call returns. + * + * FLAG_SYNC_DECODE_IF_FAST: Like FLAG_SYNC_DECODE, but requests a sync decode + * be performed only if ImageLib estimates it can be completed very quickly. + * + * FLAG_ASYNC_NOTIFY: Send notifications asynchronously, even if we decode + * synchronously because of FLAG_SYNC_DECODE or FLAG_SYNC_DECODE_IF_FAST. + * + * FLAG_DECODE_NO_PREMULTIPLY_ALPHA: Do not premultiply alpha if + * it's not already premultiplied in the image data. + * + * FLAG_DECODE_NO_COLORSPACE_CONVERSION: Do not do any colorspace conversion; + * ignore any embedded profiles, and don't convert to any particular + * destination space. + * + * FLAG_CLAMP: Extend the image to the fill area by clamping image sample + * coordinates instead of by tiling. This only affects 'draw'. + * + * FLAG_HIGH_QUALITY_SCALING: A hint as to whether this image should be + * scaled using the high quality scaler. Do not set this if not drawing to + * a window or not listening to invalidations. Passing this flag will do two + * things: 1) request a decode of the image at the size asked for by the + * caller if one isn't already started or complete, and 2) allows a decoded + * frame of any size (it could be neither the requested size, nor the + * intrinsic size) to be substituted. + * + * FLAG_BYPASS_SURFACE_CACHE: Forces drawing to happen rather than taking + * cached rendering from the surface cache. This is used when we are printing, + * for example, where we want the vector commands from VectorImages to end up + * in the PDF output rather than a cached rendering at screen resolution. + * + * FLAG_FORCE_PRESERVEASPECTRATIO_NONE: Force scaling this image + * non-uniformly if necessary. This flag is for vector image only. A raster + * image should ignore this flag. While drawing a vector image with this + * flag, do not force uniform scaling even if its root node has a + * preserveAspectRatio attribute that would otherwise require uniform + * scaling , such as xMinYMin/ xMidYMin. Always scale the graphic content of + * the given image non-uniformly if necessary such that the image's + * viewBox (if specified or implied by height/width attributes) exactly + * matches the viewport rectangle. + * + * FLAG_FORCE_UNIFORM_SCALING: Signal to ClippedImage::OptimalSizeForDest that + * its returned size can only scale the image's size *uniformly* (by the same + * factor in each dimension). We need this flag when painting border-image + * section with SVG image source-data, if the SVG image has no viewBox and no + * intrinsic size. In such a case, we synthesize a viewport for the SVG image + * (a "window into SVG space") based on the border image area, and we need to + * be sure we don't subsequently scale that viewport in a way that distorts + * its contents by stretching them more in one dimension than the other. + * + * FLAG_AVOID_REDECODE_FOR_SIZE: If there is already a raster surface + * available for this image, but it is not the same size as requested, skip + * starting a new decode for said size. + * + * FLAG_DECODE_TO_SRGB_COLORSPACE: Instead of converting the colorspace to + * the display's colorspace, use sRGB. + * + * FLAG_RECORD_BLOB: Instead of rasterizing an SVG image on the main thread, + * record the drawing commands using blob images. + */ + const unsigned long FLAG_NONE = 0x0; + const unsigned long FLAG_SYNC_DECODE = 0x1; + const unsigned long FLAG_SYNC_DECODE_IF_FAST = 0x2; + const unsigned long FLAG_ASYNC_NOTIFY = 0x4; + const unsigned long FLAG_DECODE_NO_PREMULTIPLY_ALPHA = 0x8; + const unsigned long FLAG_DECODE_NO_COLORSPACE_CONVERSION = 0x10; + const unsigned long FLAG_CLAMP = 0x20; + const unsigned long FLAG_HIGH_QUALITY_SCALING = 0x40; + const unsigned long FLAG_BYPASS_SURFACE_CACHE = 0x80; + const unsigned long FLAG_FORCE_PRESERVEASPECTRATIO_NONE = 0x100; + const unsigned long FLAG_FORCE_UNIFORM_SCALING = 0x200; + const unsigned long FLAG_AVOID_REDECODE_FOR_SIZE = 0x400; + const unsigned long FLAG_DECODE_TO_SRGB_COLORSPACE = 0x800; + const unsigned long FLAG_RECORD_BLOB = 0x1000; + + /** + * A constant specifying the default set of decode flags (i.e., the default + * values for FLAG_DECODE_*). + */ + const unsigned long DECODE_FLAGS_DEFAULT = 0; + + /** + * A constant specifying the decode flags recommended to be used when + * re-encoding an image, or with the clipboard. + */ + const unsigned long DECODE_FLAGS_FOR_REENCODE = + FLAG_DECODE_NO_PREMULTIPLY_ALPHA | FLAG_DECODE_TO_SRGB_COLORSPACE; + + /** + * Constants for specifying various "special" frames. + * + * FRAME_FIRST: The first frame + * FRAME_CURRENT: The current frame + * + * FRAME_MAX_VALUE should be set to the value of the maximum constant above, + * as it is used for ensuring that a valid value was passed in. + */ + const unsigned long FRAME_FIRST = 0; + const unsigned long FRAME_CURRENT = 1; + const unsigned long FRAME_MAX_VALUE = 1; + + /** + * Get a surface for the given frame. This may be a platform-native, + * optimized surface, so you cannot inspect its pixel data. If you + * need that, use SourceSurface::GetDataSurface. + * + * @param aWhichFrame Frame specifier of the FRAME_* variety. + * @param aFlags Flags of the FLAG_* variety + */ + [noscript, notxpcom] TempRefSourceSurface getFrame(in uint32_t aWhichFrame, + in uint32_t aFlags); + + /** + * Get a surface for the given frame at the specified size. Matching the + * requested size is best effort; it's not guaranteed that the surface you get + * will be a perfect match. (Some reasons you may get a surface of a different + * size include: if you requested upscaling, if downscale-during-decode is + * disabled, or if you didn't request the first frame.) + * + * @param aSize The desired size. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + * @param aFlags Flags of the FLAG_* variety + */ + [noscript, notxpcom] TempRefSourceSurface getFrameAtSize([const] in nsIntSize aSize, + in uint32_t aWhichFrame, + in uint32_t aFlags); + + /** + * Returns true if this image will draw opaquely right now if asked to draw + * with FLAG_HIGH_QUALITY_SCALING and otherwise default flags. If this image + * (when decoded) is opaque but no decoded frames are available then + * willDrawOpaqueNow will return false. + */ + [noscript, notxpcom] boolean willDrawOpaqueNow(); + + /** + * Returns true if this image has a frame and the frame currently has a + * least 1 decoded pixel. Only valid for raster images. + */ + [noscript, notxpcom] boolean hasDecodedPixels(); + + /** + * @return true if getImageContainer() is expected to return a valid + * ImageContainer when passed the given @Renderer and @Flags + * parameters. + */ + [noscript, notxpcom] boolean isImageContainerAvailable(in WindowRenderer aRenderer, + in uint32_t aFlags); + + /** + * Attempts to find a WebRenderImageProvider containing the current frame at + * the given size. Match the requested size is best effort; it's not + * guaranteed that the surface you get will be a perfect match. (Some reasons + * you may get a surface of a different size include: if you requested + * upscaling, or if downscale-during-decode is disabled.) + * + * @param aRenderer The WindowRenderer which will be used to render the + * ImageContainer. + * @param aSVGContext If specified, SVG-related rendering context, such as + * overridden attributes on the image document's root + * node, and the size of the viewport that the full image + * would occupy. Ignored for raster images. + * @param aFlags Decoding / drawing flags (in other words, FLAG_* flags). + * Currently only FLAG_SYNC_DECODE and FLAG_SYNC_DECODE_IF_FAST + * are supported. + * @param aProvider Return value for WebRenderImageProvider for the current + * frame. May be null depending on the draw result. + * @return The draw result for the current frame. + */ + [noscript, notxpcom] ImgDrawResult getImageProvider(in WindowRenderer aRenderer, + [const] in nsIntSize aSize, + [const] in SVGImageContext aSVGContext, + [const] in MaybeImageIntRegion aRegion, + in uint32_t aFlags, + out WebRenderImageProvider aProvider); + + /** + * Draw the requested frame of this image onto the context specified. + * + * Drawing an image involves scaling it to a certain size (which may be + * implemented as a "smart" scale by substituting an HQ-scaled frame or + * rendering at a high DPI), and then selecting a region of that image to + * draw. That region is drawn onto the graphics context and in the process + * transformed by the context matrix, which determines the final area that is + * filled. The basic process looks like this: + * + * +------------------+ + * | Image | + * | | + * | intrinsic width | + * | X | + * | intrinsic height | + * +------------------+ + * / \ + * / \ + * / (scale to aSize) \ + * / \ + * +----------------------------+ + * | | + * | Scaled Image | + * | aSize.width X aSize.height | + * | | + * | +---------+ | + * | | aRegion | | + * | +---------+ | + * +-------(---------(----------+ + * | | + * / \ + * | (transform | + * / by aContext \ + * | matrix) | + * / \ + * +---------------------+ + * | | + * | Fill Rect | + * | | + * +---------------------+ + * + * The region may extend outside of the scaled image's boundaries. It's + * actually a region in tiled image space, which is formed by tiling the + * scaled image infinitely in every direction. Drawing with a region larger + * than the scaled image thus causes the filled area to contain multiple tiled + * copies of the image, which looks like this: + * + * .................................................... + * : : : : + * : Tile : Tile : Tile : + * : +------------[aRegion]------------+ : + * :........|.......:................:........|.......: + * : | : : | : + * : Ti|le : Scaled Image : Ti|le : + * : | : : | : + * :........|.......:................:........|.......: + * : +---------------------------------+ : + * : Ti|le : Tile : Ti|le : + * : / : : \ : + * :......(.........:................:..........).....: + * | | + * / \ + * | (transform by aContext matrix) | + * / \ + * +---------------------------------------------+ + * | : : | + * |.....:.................................:.....| + * | : : | + * | : Tiled Fill : | + * | : : | + * |.....:.................................:.....| + * | : : | + * +---------------------------------------------+ + * + * + * @param aContext The Thebes context to draw the image to. + * @param aSize The size to which the image should be scaled before drawing. + * This requirement may be satisfied using HQ scaled frames, + * selecting from different resolution layers, drawing at a + * higher DPI, or just performing additional scaling on the + * graphics context. Callers can use optimalImageSizeForDest() + * to determine the best choice for this parameter if they have + * no special size requirements. + * @param aRegion The region in tiled image space which will be drawn onto the + * graphics context. aRegion is in the coordinate space of the + * image after it has been scaled to aSize - that is, the image + * is scaled first, and then aRegion is applied. When aFlags + * includes FLAG_CLAMP, the image will be extended to this area + * by clamping image sample coordinates. Otherwise, the image + * will be automatically tiled as necessary. aRegion can also + * optionally contain a second region which restricts the set + * of pixels we're allowed to sample from when drawing; this + * is only of use to callers which need to draw with pixel + * snapping. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + * @param aSamplingFilter The filter to be used if we're scaling the image. + * @param aSVGContext If specified, SVG-related rendering context, such as + * overridden attributes on the image document's root + * node, and the size of the viewport that the full image + * would occupy. Ignored for raster images. + * @param aFlags Flags of the FLAG_* variety + * @return A ImgDrawResult value indicating whether and to what degree the + * drawing operation was successful. + */ + [noscript, notxpcom] ImgDrawResult + draw(in gfxContext aContext, + [const] in nsIntSize aSize, + [const] in ImageRegion aRegion, + in uint32_t aWhichFrame, + in SamplingFilter aSamplingFilter, + [const] in SVGImageContext aSVGContext, + in uint32_t aFlags, + in float aOpacity); + + /* + * Ensures that an image is decoding. Calling this function guarantees that + * the image will at some point fire off decode notifications. Images that + * can be decoded "quickly" according to some heuristic will be decoded + * synchronously. + * + * @param aFlags Flags of the FLAG_* variety. Only FLAG_ASYNC_NOTIFY + * is accepted; all others are ignored. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + */ + [noscript] void startDecoding(in uint32_t aFlags, in uint32_t aWhichFrame); + +%{C++ + nsresult StartDecoding(uint32_t aFlags) { + return StartDecoding(aFlags, FRAME_CURRENT); + } +%} + + /* + * Exactly like startDecoding above except returns whether the current frame + * of the image is complete or not. + * + * @param aFlags Flags of the FLAG_* variety. Only FLAG_ASYNC_NOTIFY + * is accepted; all others are ignored. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + */ + [noscript, notxpcom] boolean startDecodingWithResult(in uint32_t aFlags, in uint32_t aWhichFrame); + +%{C++ + bool StartDecodingWithResult(uint32_t aFlags) { + return StartDecodingWithResult(aFlags, FRAME_CURRENT); + } +%} + + /* + * This method triggers decoding for an image, but unlike startDecoding() it + * enables the caller to provide more detailed information about the decode + * request. + * + * @param aFlags Flags of the FLAG_* variety. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + * @return DECODE_SURFACE_AVAILABLE if is a surface that satisfies the + * request and it is fully decoded. + * DECODE_REQUESTED if we requested a decode. + * DECODE_REQUEST_FAILED if we failed to request a decode. This means + * that either there is an error in the image or we cannot allocate a + * surface that big. + */ + cenum DecodeResult : 8 { + DECODE_SURFACE_AVAILABLE = 0, + DECODE_REQUESTED = 1, + DECODE_REQUEST_FAILED = 2 + }; + [noscript, notxpcom] imgIContainer_DecodeResult requestDecodeWithResult(in uint32_t aFlags, in uint32_t aWhichFrame); + +%{C++ + DecodeResult RequestDecodeWithResult(uint32_t aFlags) { + return RequestDecodeWithResult(aFlags, FRAME_CURRENT); + } +%} + + /* + * This method triggers decoding for an image, but unlike startDecoding() it + * enables the caller to provide more detailed information about the decode + * request. + * + * @param aSize The size to which the image should be scaled while decoding, + * if possible. If the image cannot be scaled to this size while + * being decoded, it will be decoded at its intrinsic size. + * @param aFlags Flags of the FLAG_* variety. + * @param aWhichFrame Frame specifier of the FRAME_* variety. + */ + [noscript] void requestDecodeForSize([const] in nsIntSize aSize, + in uint32_t aFlags, + in uint32_t aWhichFrame); + +%{C++ + nsresult RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags) { + return RequestDecodeForSize(aSize, aFlags, FRAME_CURRENT); + } +%} + + /** + * Increments the lock count on the image. An image will not be discarded + * as long as the lock count is nonzero. Note that it is still possible for + * the image to be undecoded if decode-on-draw is enabled and the image + * was never drawn. + * + * Upon instantiation images have a lock count of zero. + */ + void lockImage(); + + /** + * Decreases the lock count on the image. If the lock count drops to zero, + * the image is allowed to discard its frame data to save memory. + * + * Upon instantiation images have a lock count of zero. It is an error to + * call this method without first having made a matching lockImage() call. + * In other words, the lock count is not allowed to be negative. + */ + void unlockImage(); + + /** + * If this image is unlocked, discard its decoded data. If the image is + * locked or has already been discarded, do nothing. + */ + void requestDiscard(); + + /** + * Indicates that this imgIContainer has been triggered to update + * its internal animation state. Likely this should only be called + * from within nsImageFrame or objects of similar type. + */ + [notxpcom] void requestRefresh([const] in TimeStamp aTime); + + /** + * Animation mode Constants + * 0 = normal + * 1 = don't animate + * 2 = loop once + */ + const short kNormalAnimMode = 0; + const short kDontAnimMode = 1; + const short kLoopOnceAnimMode = 2; + + attribute unsigned short animationMode; + + /* Methods to control animation */ + void resetAnimation(); + + /* + * Returns an index for the requested animation frame (either FRAME_FIRST or + * FRAME_CURRENT). + * + * The units of the index aren't specified, and may vary between different + * types of images. What you can rely on is that on all occasions when + * getFrameIndex(FRAME_CURRENT) returns a certain value, + * draw(..FRAME_CURRENT..) will draw the same frame. The same holds for + * FRAME_FIRST as well. + * + * @param aWhichFrame Frame specifier of the FRAME_* variety. + */ + [notxpcom] float getFrameIndex(in uint32_t aWhichFrame); + + /* + * Returns the inherent orientation of the image, as described in the image's + * metadata (e.g. EXIF). + */ + [notxpcom] Orientation getOrientation(); + + /* + * Returns the intrinsic resolution of the image, or 1.0 if the image doesn't + * declare any. + */ + [notxpcom] ImageResolution getResolution(); + + /* + * Returns the delay, in ms, between the first and second frame. If this + * returns 0, there is no delay between first and second frame (i.e., this + * image could render differently whenever it draws). + * + * If this image is not animated, or not known to be animated (see attribute + * animated), returns -1. + */ + [notxpcom] int32_t getFirstFrameDelay(); + + /* + * If this is an animated image that hasn't started animating already, this + * sets the animation's start time to the indicated time. + * + * This has no effect if the image isn't animated or it has started animating + * already; it also has no effect if the image format doesn't care about + * animation start time. + * + * In all cases, animation does not actually begin until startAnimation(), + * resetAnimation(), or requestRefresh() is called for the first time. + */ + [notxpcom] void setAnimationStartTime([const] in TimeStamp aTime); + + /* + * Given an invalidation rect in the coordinate system used by the decoder, + * returns an invalidation rect in image space. + * + * This is the identity transformation in most cases, but the result can + * differ if the image is wrapped by an ImageWrapper that changes its size + * or orientation. + */ + [notxpcom] nsIntRectByVal + getImageSpaceInvalidationRect([const] in nsIntRect aRect); + + /* + * Removes any ImageWrappers and returns the unwrapped base image. + */ + [notxpcom, nostdcall] TempRefImgIContainer unwrap(); + + /* + * Propagate the use counters (if any) from this container to the passed in + * document. + */ + [noscript, notxpcom] void propagateUseCounters(in Document aReferencingDocument); + + /* + * Called when media feature values that apply to all documents (such as + * those based on system metrics) have changed. If this image is a type + * that can respond to media queries (i.e., an SVG image), this function + * is overridden to handle restyling and invalidating the image. + */ + [notxpcom, nostdcall] void mediaFeatureValuesChangedAllDocuments([const] in MediaFeatureChange aChange); + + /* + * Get the set of sizes the image can decode to natively. + */ + [nostdcall] Array getNativeSizes(); + + [nostdcall, notxpcom] size_t getNativeSizesLength(); +}; diff --git a/image/imgIContainerDebug.idl b/image/imgIContainerDebug.idl new file mode 100644 index 0000000000..426ae1df85 --- /dev/null +++ b/image/imgIContainerDebug.idl @@ -0,0 +1,25 @@ +/** -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * This interface is used in debug builds (and only there) in + * order to let automatic tests running JavaScript access + * internal state of imgContainers. This lets us test + * things like animation. + */ +[scriptable, builtinclass, uuid(52cbb839-6e63-4a70-b21a-1db4ca706c49)] +interface imgIContainerDebug : nsISupports +{ + /** + * The # of frames this imgContainer has been notified about. + * That is equal to the # of times the animation timer has + * fired, and is usually equal to the # of frames actually + * drawn (but actual drawing might be disabled). + */ + readonly attribute uint32_t framesNotified; +}; diff --git a/image/imgIEncoder.idl b/image/imgIEncoder.idl new file mode 100644 index 0000000000..c458b9cf4c --- /dev/null +++ b/image/imgIEncoder.idl @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIAsyncInputStream.idl" +#include "nsIEventTarget.idl" + +/** + * imgIEncoder interface + */ +[scriptable, builtinclass, uuid(4baa2d6e-fee7-42df-ae3f-5fbebc0c267c)] +interface imgIEncoder : nsIAsyncInputStream +{ + // Possible values for outputOptions. Multiple values are semicolon-separated. + // + // PNG: + // ---- + // transparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // png-zlib-level=[0-9] -- default: "3" + // Overrides default from compression level for zlib. + // png-filter=[no_filters|none|sub|up|avg|paeth|fast|all] -- default: "sub" + // Overrides default filter. + // + // + // APNG: + // ----- + // The following options can be used with startImageEncode(): + // + // transparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // skipfirstframe=[yes|no] -- default: "no" + // Controls display of the first frame in animations. PNG-only clients + // always display the first frame (and only that frame). + // frames=# -- default: "1" + // Total number of frames in the image. The first frame, even if skipped, + // is always included in the count. + // plays=# -- default: "0" + // Number of times to play the animation sequence. "0" will repeat + // forever. + // + // The following options can be used for each frame, with addImageFrame(): + // + // transparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // delay=# -- default: "500" + // Number of milliseconds to display the frame, before moving to the next + // frame. + // dispose=[none|background|previous] -- default: "none" + // What to do with the image's canvas before rendering the next frame. + // See APNG spec. + // blend=[source|over] -- default: "source" + // How to render the new frame on the canvas. See APNG spec. + // xoffset=# -- default: "0" + // yoffset=# -- default: "0" + // Where to draw the frame, relative to the canvas. + // + // + // JPEG: + // ----- + // + // quality=# -- default: "92" + // Quality of compression, 0-100 (worst-best). + // Quality >= 90 prevents down-sampling of the color channels. + // + // + // WEBP: + // ----- + // + // quality=# -- default: "92" + // Quality of compression, 0-100 (worst-best). + + + // Possible values for input format (note that not all image formats + // support saving alpha channels): + + // Input is RGB each pixel is represented by three bytes: + // R, G, and B (in that order, regardless of host endianness) + const uint32_t INPUT_FORMAT_RGB = 0; + + // Input is RGB each pixel is represented by four bytes: + // R, G, and B (in that order, regardless of host endianness). + // POST-MULTIPLIED alpha us used (50% transparent red is 0xff000080) + const uint32_t INPUT_FORMAT_RGBA = 1; + + // Input is host-endian ARGB: On big-endian machines each pixel is therefore + // ARGB, and for little-endian machiens (Intel) each pixel is BGRA + // (This is used by canvas to match it's internal representation) + // + // PRE-MULTIPLIED alpha is used (That is, 50% transparent red is 0x80800000, + // not 0x80ff0000 + const uint32_t INPUT_FORMAT_HOSTARGB = 2; + + /* data - list of bytes in the format specified by inputFormat + * width - width in pixels + * height - height in pixels + * stride - number of bytes per row in the image + * Normally (width*3) or (width*4), depending on your input format, + * but some data uses padding at the end of each row, which would + * be extra. + * inputFormat - one of INPUT_FORMAT_* specifying the format of data + * outputOptions - semicolon-delimited list of name=value pairs that can + * give options to the output encoder. Options are encoder- + * specific. Just give empty string for default behavior. + */ + void initFromData([array, size_is(length), const] in uint8_t data, + in unsigned long length, + in uint32_t width, + in uint32_t height, + in uint32_t stride, + in uint32_t inputFormat, + in AString outputOptions); + + /* + * For encoding images which may contain multiple frames, the 1-shot + * initFromData() interface is too simplistic. The alternative is to + * use startImageEncode(), call addImageFrame() one or more times, and + * then finish initialization with endImageEncode(). + * + * The arguments are basically the same as in initFromData(). + */ + void startImageEncode(in uint32_t width, + in uint32_t height, + in uint32_t inputFormat, + in AString outputOptions); + + void addImageFrame( [array, size_is(length), const] in uint8_t data, + in unsigned long length, + in uint32_t width, + in uint32_t height, + in uint32_t stride, + in uint32_t frameFormat, + in AString frameOptions); + + void endImageEncode(); + + /* + * Sometimes an encoder can contain another encoder and direct access + * to its buffer is necessary. It is only safe to assume that the buffer + * returned from getImageBuffer() is of size equal to getImageBufferUsed(). + */ + [noscript] unsigned long getImageBufferUsed(); + [noscript] charPtr getImageBuffer(); +}; diff --git a/image/imgILoader.idl b/image/imgILoader.idl new file mode 100644 index 0000000000..a8514790a0 --- /dev/null +++ b/image/imgILoader.idl @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface imgINotificationObserver; +interface imgIRequest; + +interface nsIChannel; +interface nsILoadGroup; +interface nsIPrincipal; +interface nsIStreamListener; +interface nsIURI; +interface nsIReferrerInfo; + +interface nsISimpleEnumerator; + +webidl Document; + +#include "nsIRequest.idl" // for nsLoadFlags +#include "nsIContentPolicy.idl" // for nsContentPolicyType + +/** + * imgILoader interface + * + * @author Stuart Parmenter + * @version 0.3 + * @see imagelib2 + */ +[scriptable, builtinclass, uuid(e61377d2-910e-4c65-a64b-428d150e1fd1)] +interface imgILoader : nsISupports +{ + // Extra flags to pass to loadImage if you want a load to use CORS + // validation. + const unsigned long LOAD_CORS_ANONYMOUS = 1 << 16; + const unsigned long LOAD_CORS_USE_CREDENTIALS = 1 << 17; + + /** + * Start the load and decode of an image. + * @param aURI the URI to load + * @param aInitialDocumentURI the URI that 'initiated' the load -- used for + * 3rd party cookie blocking + * @param aReferrerInfo the referrer info to compute sending referrer. + * @param aLoadingPrincipal the principal of the loading document + * @param aLoadGroup Loadgroup to put the image load into + * @param aObserver the observer (may be null) + * @param aLoadingDocument loading document + * @param aLoadFlags Load flags for the request + * @param aCacheKey cache key to use for a load if the original + * image came from a request that had post data + * @param aContentPolicyType [optional] the nsContentPolicyType to + * use for this load. Defaults to + * nsIContentPolicy::TYPE_IMAGE + + + * ImageLib does NOT keep a strong ref to the observer; this prevents + * reference cycles. This means that callers of loadImage should + * make sure to Cancel() the resulting request before the observer + * goes away. + */ + imgIRequest loadImageXPCOM(in nsIURI aURI, + in nsIURI aInitialDocumentURL, + in nsIReferrerInfo aReferrerInfo, + in nsIPrincipal aLoadingPrincipal, + in nsILoadGroup aLoadGroup, + in imgINotificationObserver aObserver, + in Document aLoadingDocument, + in nsLoadFlags aLoadFlags, + in nsISupports cacheKey, + [optional] + in nsContentPolicyType aContentPolicyType); + + /** + * Start the load and decode of an image. + * @param aChannel the channel to load the image from. This must + * already be opened before this method is called, and there + * must have been no OnDataAvailable calls for it yet. + * @param aObserver the observer (may be null) + * @param aLoadingDocument loading document + * @param aListener [out] + * A listener that you must send the channel's notifications and data + * to. Can be null, in which case imagelib has found a cached image + * and is not interested in the data. @aChannel will be canceled for + * you in this case. + * + * ImageLib does NOT keep a strong ref to the observer; this prevents + * reference cycles. This means that callers of loadImageWithChannel should + * make sure to Cancel() the resulting request before the observer goes away. + */ + imgIRequest loadImageWithChannelXPCOM(in nsIChannel aChannel, + in imgINotificationObserver aObserver, + in Document aLoadingDocument, + out nsIStreamListener aListener); +}; diff --git a/image/imgINotificationObserver.idl b/image/imgINotificationObserver.idl new file mode 100644 index 0000000000..bf65713227 --- /dev/null +++ b/image/imgINotificationObserver.idl @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface imgIRequest; + +%{C++ +#include "nsRect.h" +%} + +[ptr] native nsIntRect(nsIntRect); + +[scriptable, builtinclass, uuid(03da5641-a333-454a-a859-036d0bb683b7)] +interface imgINotificationObserver : nsISupports +{ + // GetWidth() and GetHeight() can now be used to retrieve the size of the + // image. + const long SIZE_AVAILABLE = 1; + + // A region of the image (indicated by the |aRect| argument to |notify|) has + // changed, and needs to be redrawn. This is triggered both for incremental + // rendering as the image gets decoded and for changes due to animation. + const long FRAME_UPDATE = 2; + + // The first frame of the image is now decoded and ready to draw. + const long FRAME_COMPLETE = 3; + + // The entire image has been loaded. That doesn't mean that it has been + // decoded, but it does mean that imgIContainer::Draw is guaranteed to succeed + // (modulo decode errors, at least) if you specify FLAG_SYNC_DECODE. + const long LOAD_COMPLETE = 4; + + // The entire image has been decoded. + const long DECODE_COMPLETE = 5; + + // The decoded version of the image has been discarded. Content should never + // change as a result of this notification - discarding is an implementation + // detail. This notification should normally only be observed by tests. + const long DISCARD = 6; + + // The image was drawn without being locked. This notification is part of the + // image locking mechanism that prevents visible images from being discarded; + // generally only image locking code needs to observe it. + const long UNLOCKED_DRAW = 7; + + // The image is animated. + const long IS_ANIMATED = 8; + + // The image is transparent. + const long HAS_TRANSPARENCY = 9; + + [noscript, notxpcom, nostdcall] + void notify(in imgIRequest aProxy, in long aType, + [const] in nsIntRect aRect); +}; diff --git a/image/imgIRequest.idl b/image/imgIRequest.idl new file mode 100644 index 0000000000..721beb3a5a --- /dev/null +++ b/image/imgIRequest.idl @@ -0,0 +1,276 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIRequest.idl" +#include "imgIContainer.idl" + +//interface imgIContainer; +interface imgINotificationObserver; +interface nsIURI; +interface nsIPrincipal; +interface nsIReferrerInfo; + +/** + * imgIRequest interface + * + * @author Stuart Parmenter + * @version 0.1 + * @see imagelib2 + */ +[scriptable, builtinclass, uuid(db0a945c-3883-424a-98d0-2ee0523b0255)] +interface imgIRequest : nsIRequest +{ + /** + * the image container... + * @return the image object associated with the request. + * @attention NEED DOCS + */ + readonly attribute imgIContainer image; + + /** + * Provider ID for image providers created by this image. + */ + [infallible] readonly attribute unsigned long providerId; + + /** + * The principal for the document that loaded this image. Used when trying to + * validate a CORS image load. + */ + [infallible] readonly attribute nsIPrincipal triggeringPrincipal; + + /** + * Bits set in the return value from imageStatus + * @name statusflags + * + * Meanings: + * + * STATUS_NONE: Nothing to report. + * + * STATUS_SIZE_AVAILABLE: We received enough image data + * from the network or filesystem that we know the width + * and height of the image, and have thus called SetSize() + * on the container. + * + * STATUS_LOAD_COMPLETE: The data has been fully loaded + * to memory, but not necessarily fully decoded. + * + * STATUS_ERROR: An error occurred loading the image. + * + * STATUS_FRAME_COMPLETE: The first frame has been + * completely decoded. + * + * STATUS_DECODE_COMPLETE: The whole image has been decoded. + * + * STATUS_IS_ANIMATED: The image is animated. + * + * STATUS_HAS_TRANSPARENCY: The image is partially or completely transparent. + */ + //@{ + const long STATUS_NONE = 0x0; + const long STATUS_SIZE_AVAILABLE = 0x1; + const long STATUS_LOAD_COMPLETE = 0x2; + const long STATUS_ERROR = 0x4; + const long STATUS_FRAME_COMPLETE = 0x8; + const long STATUS_DECODE_COMPLETE = 0x10; + const long STATUS_IS_ANIMATED = 0x20; + const long STATUS_HAS_TRANSPARENCY = 0x40; + //@} + + /** + * Status flags of the STATUS_* variety. + */ + readonly attribute unsigned long imageStatus; + + /* + * Actual error code that generated a STATUS_ERROR imageStatus + * (see xpcom/base/ErrorList.h) + */ + [noscript] readonly attribute nsresult imageErrorCode; + + /** + * The URI the image load was started with. Note that this might not be the + * actual URI for the image (e.g. if HTTP redirects happened during the + * load). + */ + [infallible] readonly attribute nsIURI URI; + + /** + * The URI of the resource we ended up loading after all redirects, etc. + */ + readonly attribute nsIURI finalURI; + + readonly attribute imgINotificationObserver notificationObserver; + + readonly attribute string mimeType; + + /** + * The filename that should be used when saving the image. This is determined + * from the Content-Disposition, if present, or the uri of the image. This + * filename should be validated using nsIMIMEService::GetValidFilenameForSaving + * before creating the file. + */ + readonly attribute ACString fileName; + + /** + * Clone this request; the returned request will have aObserver as the + * observer. aObserver will be notified synchronously (before the clone() + * call returns) with all the notifications that have already been dispatched + * for this image load. + */ + imgIRequest clone(in imgINotificationObserver aObserver); + + /** + * The principal gotten from the channel the image was loaded from. + */ + readonly attribute nsIPrincipal imagePrincipal; + + /** + * true if the loading of the image required cross-origin redirects. + */ + readonly attribute bool hadCrossOriginRedirects; + + /** + * Whether the request is multipart (ie, multipart/x-mixed-replace) + */ + readonly attribute bool multipart; + + /** + * The CORS mode that this image was loaded with (a mozilla::CORSMode). + */ + readonly attribute long CORSMode; + + /** + * The referrer that this image was loaded with. + */ + readonly attribute nsIReferrerInfo referrerInfo; + + /** + * Cancels this request as in nsIRequest::Cancel(); further, also nulls out + * decoderObserver so it gets no further notifications from us. + * + * NOTE: You should not use this in any new code; instead, use cancel(). Note + * that cancel() is asynchronous, which means that some time after you call + * it, the listener/observer will get an OnStopRequest(). This means that, if + * you're the observer, you can't call cancel() from your destructor. + */ + void cancelAndForgetObserver(in nsresult aStatus); + + /** + * Requests a synchronous decode for the image. + * + * imgIContainer has a startDecoding() method, but callers may want to request + * a decode before the container has necessarily been instantiated. Calling + * startDecoding() on the imgIRequest simply forwards along the request if the + * container already exists, or calls it once the container becomes available + * if it does not yet exist. + */ + void startDecoding(in uint32_t aFlags); + + /** + * Exactly like startDecoding above except returns whether the current frame + * of the image is complete or not. + * + * @param aFlags Flags of the FLAG_* variety. Only FLAG_ASYNC_NOTIFY + * is accepted; all others are ignored. + */ + [noscript, notxpcom] boolean startDecodingWithResult(in uint32_t aFlags); + + /** + * This method triggers decoding for an image, but unlike startDecoding() it + * enables the caller to provide more detailed information about the decode + * request. + * + * @param aFlags Flags of the FLAG_* variety. + * @return DECODE_SURFACE_AVAILABLE if is a surface that satisfies the + * request and it is fully decoded. + * DECODE_REQUESTED if we requested a decode. + * DECODE_REQUEST_FAILED if we failed to request a decode. This means + * that either there is an error in the image or we cannot allocate a + * surface that big. + */ + [noscript, notxpcom] imgIContainer_DecodeResult requestDecodeWithResult(in uint32_t aFlags); +/*%{C++ + DecodeResult RequestDecodeWithResult(uint32_t aFlags); +%}*/ + + /** + * Returns true if there is a image and the image has a frame and the frame + * currently has a least 1 decoded pixel. Only valid for raster images. + */ + [noscript, notxpcom] boolean hasDecodedPixels(); + + /** + * Locks an image. If the image does not exist yet, locks it once it becomes + * available. The lock persists for the lifetime of the imgIRequest (until + * unlockImage is called) even if the underlying image changes. + * + * If you don't call unlockImage() by the time this imgIRequest goes away, it + * will be called for you automatically. + * + * @see imgIContainer::lockImage for documentation of the underlying call. + */ + void lockImage(); + + /** + * Unlocks an image. + * + * @see imgIContainer::unlockImage for documentation of the underlying call. + */ + void unlockImage(); + + /** + * If this image is unlocked, discard the image's decoded data. If the image + * is locked or is already discarded, do nothing. + */ + void requestDiscard(); + + /** + * If this request is for an animated image, the method creates a new + * request which contains the current frame of the image. + * Otherwise returns the same request. + */ + imgIRequest getStaticRequest(); + + /** + * Requests that the image animate (if it has an animation). + * + * @see Image::IncrementAnimationConsumers for documentation of the + * underlying call. + */ + void incrementAnimationConsumers(); + + /** + * Tell the image it can forget about a request that the image animate. + * + * @see Image::DecrementAnimationConsumers for documentation of the + * underlying call. + */ + void decrementAnimationConsumers(); + + /** + * Request loading priority boost to requested category, each category + * of request increases priority only one time. + * + * CATEGORY_FRAME_INIT: increase priority when the imgRequest is associated + * with an nsImageFrame. + * + * CATEGORY_FRAME_STYLE: increase priority when the imgRequest is for a CSS + * background-image, list-style-image, etc. on a ComputedStyle, and a frame + * has been assigned this ComputedStyle. + * + * CATEGORY_SIZE_QUERY: increase priority when size decoding is necessary to + * determine the layout size of an associated nsImageFrame. + * + * CATEGORY_DISPLAY: increase priority when the image is about to be displayed + * in the viewport. + */ + const uint32_t CATEGORY_FRAME_INIT = 1 << 0; + const uint32_t CATEGORY_FRAME_STYLE = 1 << 1; + const uint32_t CATEGORY_SIZE_QUERY = 1 << 2; + const uint32_t CATEGORY_DISPLAY = 1 << 3; + void boostPriority(in uint32_t aCategory); +}; diff --git a/image/imgIScriptedNotificationObserver.idl b/image/imgIScriptedNotificationObserver.idl new file mode 100644 index 0000000000..54769e38fc --- /dev/null +++ b/image/imgIScriptedNotificationObserver.idl @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface imgIRequest; + +[scriptable, uuid(10be55b3-2029-41a7-a975-538efed250ed)] +interface imgIScriptedNotificationObserver : nsISupports +{ + void sizeAvailable(in imgIRequest aRequest); + void frameUpdate(in imgIRequest aRequest); + void frameComplete(in imgIRequest aRequest); + void loadComplete(in imgIRequest aRequest); + void decodeComplete(in imgIRequest aRequest); + void discard(in imgIRequest aRequest); + void isAnimated(in imgIRequest aRequest); + void hasTransparency(in imgIRequest aRequest); +}; diff --git a/image/imgITools.idl b/image/imgITools.idl new file mode 100644 index 0000000000..f635f21162 --- /dev/null +++ b/image/imgITools.idl @@ -0,0 +1,207 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIChannel; +interface nsIEventTarget; +interface nsIInputStream; +interface nsIURI; +interface imgIContainer; +interface imgILoader; +interface imgICache; +interface imgIScriptedNotificationObserver; +interface imgINotificationObserver; +interface imgIContainerCallback; + +webidl Document; + +[scriptable, builtinclass, uuid(4c2383a4-931c-484d-8c4a-973590f66e3f)] +interface imgITools : nsISupports +{ + /** + * decodeImageFromBuffer + * Caller provides an buffer, a buffer size and a mimetype. We read from + * the stream and decompress it (according to the specified mime type) and + * return the resulting imgIContainer. + * + * @param aBuffer + * Data in memory. + * @param aSize + * Buffer size. + * @param aMimeType + * Type of image in the stream. + */ + imgIContainer decodeImageFromBuffer(in string aBuffer, + in unsigned long aSize, + in ACString aMimeType); + + /** + * decodeImageFromArrayBuffer + * Caller provides an ArrayBuffer and a mimetype. We read from + * the stream and decompress it (according to the specified mime type) and + * return the resulting imgIContainer. + * + * @param aArrayBuffer + * An ArrayBuffer. + * @param aMimeType + * Type of image in the stream. + */ + [implicit_jscontext] + imgIContainer decodeImageFromArrayBuffer(in jsval aArrayBuffer, + in ACString aMimeType); + + /** + * decodeImageFromChannelAsync + * See decodeImage. The main difference between this method and decodeImage + * is that here the operation is done async on a thread from the decode + * pool. When the operation is completed, the callback is executed with the + * result. + * + * @param aURI + * The original URI of the image + * @param aChannel + * Channel to the image to be decoded. + * @param aCallback + * The callback is executed when the imgContainer is fully created. + * @param aObserver + * Optional observer for the decoded image, the caller should make + * sure the observer is kept alive as long as necessary, as ImageLib + * does not keep a strong reference to the observer. + */ + void decodeImageFromChannelAsync(in nsIURI aURI, + in nsIChannel aChannel, + in imgIContainerCallback aCallback, + in imgINotificationObserver aObserver); + + /** + * decodeImageAsync + * See decodeImage. The main difference between this method and decodeImage + * is that here the operation is done async on a thread from the decode + * pool. When the operation is completed, the callback is executed with the + * result. + * + * @param aStream + * An input stream for an encoded image file. + * @param aMimeType + * Type of image in the stream. + * @param aCallback + * The callback is executed when the imgContainer is fully created. + * @param aEventTarget + * This eventTarget is used to execute aCallback + */ + void decodeImageAsync(in nsIInputStream aStream, + in ACString aMimeType, + in imgIContainerCallback aCallback, + in nsIEventTarget aEventTarget); + + /** + * encodeImage + * Caller provides an image container, and the mime type it should be + * encoded to. We return an input stream for the encoded image data. + * + * @param aContainer + * An image container. + * @param aMimeType + * Type of encoded image desired (eg "image/png"). + * @param outputOptions + * Encoder-specific output options. + */ + nsIInputStream encodeImage(in imgIContainer aContainer, + in ACString aMimeType, + [optional] in AString outputOptions); + + /** + * encodeScaledImage + * Caller provides an image container, and the mime type it should be + * encoded to. We return an input stream for the encoded image data. + * The encoded image is scaled to the specified dimensions. + * + * @param aContainer + * An image container. + * @param aMimeType + * Type of encoded image desired (eg "image/png"). + * @param aWidth, aHeight + * The size (in pixels) desired for the resulting image. Specify 0 to + * use the given image's width or height. Values must be >= 0. + * @param outputOptions + * Encoder-specific output options. + */ + nsIInputStream encodeScaledImage(in imgIContainer aContainer, + in ACString aMimeType, + in long aWidth, + in long aHeight, + [optional] in AString outputOptions); + + /** + * getImgLoaderForDocument + * Retrieve an image loader that reflects the privacy status of the given + * document. + * + * @param doc + * A document. Must not be null. + */ + imgILoader getImgLoaderForDocument(in Document doc); + + /** + * getImgLoaderForDocument + * Retrieve an image cache that reflects the privacy status of the given + * document. + * + * @param doc + * A document. Null is allowed, but must _only_ be passed + * when there is no way to obtain a relevant document for + * the current context in which a cache is desired. + */ + imgICache getImgCacheForDocument(in Document doc); + + /** + * encodeCroppedImage + * Caller provides an image container, and the mime type it should be + * encoded to. We return an input stream for the encoded image data. + * The encoded image is cropped to the specified dimensions. + * + * The given offset and size must not exceed the image bounds. + * + * @param aContainer + * An image container. + * @param aMimeType + * Type of encoded image desired (eg "image/png"). + * @param aOffsetX, aOffsetY + * The crop offset (in pixels). Values must be >= 0. + * @param aWidth, aHeight + * The size (in pixels) desired for the resulting image. Specify 0 to + * use the given image's width or height. Values must be >= 0. + * @param outputOptions + * Encoder-specific output options. + */ + nsIInputStream encodeCroppedImage(in imgIContainer aContainer, + in ACString aMimeType, + in long aOffsetX, + in long aOffsetY, + in long aWidth, + in long aHeight, + [optional] in AString outputOptions); + + /** + * Create a wrapper around a scripted notification observer (ordinarily + * imgINotificationObserver cannot be implemented from scripts). + * + * @param aObserver The scripted observer to wrap + */ + imgINotificationObserver + createScriptedObserver(in imgIScriptedNotificationObserver aObserver); +}; + +/** + * This is a companion interface for nsIAsyncInputStream::asyncWait. + */ +[function, scriptable, uuid(f195772c-a4c0-47ae-80ca-211e001c67be)] +interface imgIContainerCallback : nsISupports +{ + /* If the operation fails, aStatus will contain the error value */ + void onImageReady(in imgIContainer aImage, in nsresult aStatus); +}; diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp new file mode 100644 index 0000000000..dcfb031789 --- /dev/null +++ b/image/imgLoader.cpp @@ -0,0 +1,3334 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Undefine windows version of LoadImage because our code uses that name. +#include "mozilla/ScopeExit.h" +#include "nsIChildChannel.h" +#undef LoadImage + +#include "imgLoader.h" + +#include +#include + +#include "DecoderFactory.h" +#include "Image.h" +#include "ImageLogging.h" +#include "ReferrerInfo.h" +#include "imgRequestProxy.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/image/ImageMemoryReporter.h" +#include "mozilla/layers/CompositorManagerChild.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsComponentManagerUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsHttpChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICacheInfoChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIClassOfService.h" +#include "nsIEffectiveTLDService.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIHttpChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIMemoryReporter.h" +#include "nsINetworkPredictor.h" +#include "nsIProgressEventSink.h" +#include "nsIProtocolHandler.h" +#include "nsImageModule.h" +#include "nsMediaSniffer.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "prtime.h" + +// we want to explore making the document own the load group +// so we can associate the document URI with the load group. +// until this point, we have an evil hack: +#include "nsIHttpChannelInternal.h" +#include "nsILoadGroupChild.h" +#include "nsIDocShell.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::image; +using namespace mozilla::net; + +MOZ_DEFINE_MALLOC_SIZE_OF(ImagesMallocSizeOf) + +class imgMemoryReporter final : public nsIMemoryReporter { + ~imgMemoryReporter() = default; + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override { + MOZ_ASSERT(NS_IsMainThread()); + + layers::CompositorManagerChild* manager = + mozilla::layers::CompositorManagerChild::GetInstance(); + if (!manager || !StaticPrefs::image_mem_debug_reporting()) { + layers::SharedSurfacesMemoryReport sharedSurfaces; + FinishCollectReports(aHandleReport, aData, aAnonymize, sharedSurfaces); + return NS_OK; + } + + RefPtr self(this); + nsCOMPtr handleReport(aHandleReport); + nsCOMPtr data(aData); + manager->SendReportSharedSurfacesMemory( + [=](layers::SharedSurfacesMemoryReport aReport) { + self->FinishCollectReports(handleReport, data, aAnonymize, aReport); + }, + [=](mozilla::ipc::ResponseRejectReason&& aReason) { + layers::SharedSurfacesMemoryReport sharedSurfaces; + self->FinishCollectReports(handleReport, data, aAnonymize, + sharedSurfaces); + }); + return NS_OK; + } + + void FinishCollectReports( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize, layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + nsTArray chrome; + nsTArray content; + nsTArray uncached; + + for (uint32_t i = 0; i < mKnownLoaders.Length(); i++) { + for (imgCacheEntry* entry : mKnownLoaders[i]->mCache.Values()) { + RefPtr req = entry->GetRequest(); + RecordCounterForRequest(req, &content, !entry->HasNoProxies()); + } + MutexAutoLock lock(mKnownLoaders[i]->mUncachedImagesMutex); + for (RefPtr req : mKnownLoaders[i]->mUncachedImages) { + RecordCounterForRequest(req, &uncached, req->HasConsumers()); + } + } + + // Note that we only need to anonymize content image URIs. + + ReportCounterArray(aHandleReport, aData, chrome, "images/chrome", + /* aAnonymize */ false, aSharedSurfaces); + + ReportCounterArray(aHandleReport, aData, content, "images/content", + aAnonymize, aSharedSurfaces); + + // Uncached images may be content or chrome, so anonymize them. + ReportCounterArray(aHandleReport, aData, uncached, "images/uncached", + aAnonymize, aSharedSurfaces); + + // Report any shared surfaces that were not merged with the surface cache. + ImageMemoryReporter::ReportSharedSurfaces(aHandleReport, aData, + aSharedSurfaces); + + nsCOMPtr imgr = + do_GetService("@mozilla.org/memory-reporter-manager;1"); + if (imgr) { + imgr->EndReport(); + } + } + + static int64_t ImagesContentUsedUncompressedDistinguishedAmount() { + size_t n = 0; + for (uint32_t i = 0; i < imgLoader::sMemReporter->mKnownLoaders.Length(); + i++) { + for (imgCacheEntry* entry : + imgLoader::sMemReporter->mKnownLoaders[i]->mCache.Values()) { + if (entry->HasNoProxies()) { + continue; + } + + RefPtr req = entry->GetRequest(); + RefPtr image = req->GetImage(); + if (!image) { + continue; + } + + // Both this and EntryImageSizes measure + // images/content/raster/used/decoded memory. This function's + // measurement is secondary -- the result doesn't go in the "explicit" + // tree -- so we use moz_malloc_size_of instead of ImagesMallocSizeOf to + // prevent DMD from seeing it reported twice. + SizeOfState state(moz_malloc_size_of); + ImageMemoryCounter counter(req, image, state, /* aIsUsed = */ true); + + n += counter.Values().DecodedHeap(); + n += counter.Values().DecodedNonHeap(); + n += counter.Values().DecodedUnknown(); + } + } + return n; + } + + void RegisterLoader(imgLoader* aLoader) { + mKnownLoaders.AppendElement(aLoader); + } + + void UnregisterLoader(imgLoader* aLoader) { + mKnownLoaders.RemoveElement(aLoader); + } + + private: + nsTArray mKnownLoaders; + + struct MemoryTotal { + MemoryTotal& operator+=(const ImageMemoryCounter& aImageCounter) { + if (aImageCounter.Type() == imgIContainer::TYPE_RASTER) { + if (aImageCounter.IsUsed()) { + mUsedRasterCounter += aImageCounter.Values(); + } else { + mUnusedRasterCounter += aImageCounter.Values(); + } + } else if (aImageCounter.Type() == imgIContainer::TYPE_VECTOR) { + if (aImageCounter.IsUsed()) { + mUsedVectorCounter += aImageCounter.Values(); + } else { + mUnusedVectorCounter += aImageCounter.Values(); + } + } else if (aImageCounter.Type() == imgIContainer::TYPE_REQUEST) { + // Nothing to do, we did not get to the point of having an image. + } else { + MOZ_CRASH("Unexpected image type"); + } + + return *this; + } + + const MemoryCounter& UsedRaster() const { return mUsedRasterCounter; } + const MemoryCounter& UnusedRaster() const { return mUnusedRasterCounter; } + const MemoryCounter& UsedVector() const { return mUsedVectorCounter; } + const MemoryCounter& UnusedVector() const { return mUnusedVectorCounter; } + + private: + MemoryCounter mUsedRasterCounter; + MemoryCounter mUnusedRasterCounter; + MemoryCounter mUsedVectorCounter; + MemoryCounter mUnusedVectorCounter; + }; + + // Reports all images of a single kind, e.g. all used chrome images. + void ReportCounterArray(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, + nsTArray& aCounterArray, + const char* aPathPrefix, bool aAnonymize, + layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + MemoryTotal summaryTotal; + MemoryTotal nonNotableTotal; + + // Report notable images, and compute total and non-notable aggregate sizes. + for (uint32_t i = 0; i < aCounterArray.Length(); i++) { + ImageMemoryCounter& counter = aCounterArray[i]; + + if (aAnonymize) { + counter.URI().Truncate(); + counter.URI().AppendPrintf("", i); + } else { + // The URI could be an extremely long data: URI. Truncate if needed. + static const size_t max = 256; + if (counter.URI().Length() > max) { + counter.URI().Truncate(max); + counter.URI().AppendLiteral(" (truncated)"); + } + counter.URI().ReplaceChar('/', '\\'); + } + + summaryTotal += counter; + + if (counter.IsNotable() || StaticPrefs::image_mem_debug_reporting()) { + ReportImage(aHandleReport, aData, aPathPrefix, counter, + aSharedSurfaces); + } else { + ImageMemoryReporter::TrimSharedSurfaces(counter, aSharedSurfaces); + nonNotableTotal += counter; + } + } + + // Report non-notable images in aggregate. + ReportTotal(aHandleReport, aData, /* aExplicit = */ true, aPathPrefix, + "/", nonNotableTotal); + + // Report a summary in aggregate, outside of the explicit tree. + ReportTotal(aHandleReport, aData, /* aExplicit = */ false, aPathPrefix, "", + summaryTotal); + } + + static void ReportImage(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, const char* aPathPrefix, + const ImageMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + nsAutoCString pathPrefix("explicit/"_ns); + pathPrefix.Append(aPathPrefix); + + switch (aCounter.Type()) { + case imgIContainer::TYPE_RASTER: + pathPrefix.AppendLiteral("/raster/"); + break; + case imgIContainer::TYPE_VECTOR: + pathPrefix.AppendLiteral("/vector/"); + break; + case imgIContainer::TYPE_REQUEST: + pathPrefix.AppendLiteral("/request/"); + break; + default: + pathPrefix.AppendLiteral("/unknown="); + pathPrefix.AppendInt(aCounter.Type()); + pathPrefix.AppendLiteral("/"); + break; + } + + pathPrefix.Append(aCounter.IsUsed() ? "used/" : "unused/"); + if (aCounter.IsValidating()) { + pathPrefix.AppendLiteral("validating/"); + } + if (aCounter.HasError()) { + pathPrefix.AppendLiteral("err/"); + } + + pathPrefix.AppendLiteral("progress="); + pathPrefix.AppendInt(aCounter.Progress(), 16); + pathPrefix.AppendLiteral("/"); + + pathPrefix.AppendLiteral("image("); + pathPrefix.AppendInt(aCounter.IntrinsicSize().width); + pathPrefix.AppendLiteral("x"); + pathPrefix.AppendInt(aCounter.IntrinsicSize().height); + pathPrefix.AppendLiteral(", "); + + if (aCounter.URI().IsEmpty()) { + pathPrefix.AppendLiteral(""); + } else { + pathPrefix.Append(aCounter.URI()); + } + + pathPrefix.AppendLiteral(")/"); + + ReportSurfaces(aHandleReport, aData, pathPrefix, aCounter, aSharedSurfaces); + + ReportSourceValue(aHandleReport, aData, pathPrefix, aCounter.Values()); + } + + static void ReportSurfaces( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + const nsACString& aPathPrefix, const ImageMemoryCounter& aCounter, + layers::SharedSurfacesMemoryReport& aSharedSurfaces) { + for (const SurfaceMemoryCounter& counter : aCounter.Surfaces()) { + nsAutoCString surfacePathPrefix(aPathPrefix); + switch (counter.Type()) { + case SurfaceMemoryCounterType::NORMAL: + if (counter.IsLocked()) { + surfacePathPrefix.AppendLiteral("locked/"); + } else { + surfacePathPrefix.AppendLiteral("unlocked/"); + } + if (counter.IsFactor2()) { + surfacePathPrefix.AppendLiteral("factor2/"); + } + if (counter.CannotSubstitute()) { + surfacePathPrefix.AppendLiteral("cannot_substitute/"); + } + break; + case SurfaceMemoryCounterType::CONTAINER: + surfacePathPrefix.AppendLiteral("container/"); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown counter type"); + break; + } + + surfacePathPrefix.AppendLiteral("types="); + surfacePathPrefix.AppendInt(counter.Values().SurfaceTypes(), 16); + surfacePathPrefix.AppendLiteral("/surface("); + surfacePathPrefix.AppendInt(counter.Key().Size().width); + surfacePathPrefix.AppendLiteral("x"); + surfacePathPrefix.AppendInt(counter.Key().Size().height); + + if (!counter.IsFinished()) { + surfacePathPrefix.AppendLiteral(", incomplete"); + } + + if (counter.Values().ExternalHandles() > 0) { + surfacePathPrefix.AppendLiteral(", handles:"); + surfacePathPrefix.AppendInt( + uint32_t(counter.Values().ExternalHandles())); + } + + ImageMemoryReporter::AppendSharedSurfacePrefix(surfacePathPrefix, counter, + aSharedSurfaces); + + PlaybackType playback = counter.Key().Playback(); + if (playback == PlaybackType::eAnimated) { + if (StaticPrefs::image_mem_debug_reporting()) { + surfacePathPrefix.AppendPrintf( + " (animation %4u)", uint32_t(counter.Values().FrameIndex())); + } else { + surfacePathPrefix.AppendLiteral(" (animation)"); + } + } + + if (counter.Key().Flags() != DefaultSurfaceFlags()) { + surfacePathPrefix.AppendLiteral(", flags:"); + surfacePathPrefix.AppendInt(uint32_t(counter.Key().Flags()), + /* aRadix = */ 16); + } + + if (counter.Key().Region()) { + const ImageIntRegion& region = counter.Key().Region().ref(); + const gfx::IntRect& rect = region.Rect(); + surfacePathPrefix.AppendLiteral(", region:[ rect=("); + surfacePathPrefix.AppendInt(rect.x); + surfacePathPrefix.AppendLiteral(","); + surfacePathPrefix.AppendInt(rect.y); + surfacePathPrefix.AppendLiteral(") "); + surfacePathPrefix.AppendInt(rect.width); + surfacePathPrefix.AppendLiteral("x"); + surfacePathPrefix.AppendInt(rect.height); + if (region.IsRestricted()) { + const gfx::IntRect& restrict = region.Restriction(); + if (restrict == rect) { + surfacePathPrefix.AppendLiteral(", restrict=rect"); + } else { + surfacePathPrefix.AppendLiteral(", restrict=("); + surfacePathPrefix.AppendInt(restrict.x); + surfacePathPrefix.AppendLiteral(","); + surfacePathPrefix.AppendInt(restrict.y); + surfacePathPrefix.AppendLiteral(") "); + surfacePathPrefix.AppendInt(restrict.width); + surfacePathPrefix.AppendLiteral("x"); + surfacePathPrefix.AppendInt(restrict.height); + } + } + if (region.GetExtendMode() != gfx::ExtendMode::CLAMP) { + surfacePathPrefix.AppendLiteral(", extendMode="); + surfacePathPrefix.AppendInt(int32_t(region.GetExtendMode())); + } + surfacePathPrefix.AppendLiteral("]"); + } + + const SVGImageContext& context = counter.Key().SVGContext(); + surfacePathPrefix.AppendLiteral(", svgContext:[ "); + if (context.GetViewportSize()) { + const CSSIntSize& size = context.GetViewportSize().ref(); + surfacePathPrefix.AppendLiteral("viewport=("); + surfacePathPrefix.AppendInt(size.width); + surfacePathPrefix.AppendLiteral("x"); + surfacePathPrefix.AppendInt(size.height); + surfacePathPrefix.AppendLiteral(") "); + } + if (context.GetPreserveAspectRatio()) { + nsAutoString aspect; + context.GetPreserveAspectRatio()->ToString(aspect); + surfacePathPrefix.AppendLiteral("preserveAspectRatio=("); + LossyAppendUTF16toASCII(aspect, surfacePathPrefix); + surfacePathPrefix.AppendLiteral(") "); + } + if (auto scheme = context.GetColorScheme()) { + surfacePathPrefix.AppendLiteral("colorScheme="); + surfacePathPrefix.AppendInt(int32_t(*scheme)); + surfacePathPrefix.AppendLiteral(" "); + } + if (context.GetContextPaint()) { + const SVGEmbeddingContextPaint* paint = context.GetContextPaint(); + surfacePathPrefix.AppendLiteral("contextPaint=("); + if (paint->GetFill()) { + surfacePathPrefix.AppendLiteral(" fill="); + surfacePathPrefix.AppendInt(paint->GetFill()->ToABGR(), 16); + } + if (paint->GetFillOpacity() != 1.0) { + surfacePathPrefix.AppendLiteral(" fillOpa="); + surfacePathPrefix.AppendFloat(paint->GetFillOpacity()); + } + if (paint->GetStroke()) { + surfacePathPrefix.AppendLiteral(" stroke="); + surfacePathPrefix.AppendInt(paint->GetStroke()->ToABGR(), 16); + } + if (paint->GetStrokeOpacity() != 1.0) { + surfacePathPrefix.AppendLiteral(" strokeOpa="); + surfacePathPrefix.AppendFloat(paint->GetStrokeOpacity()); + } + surfacePathPrefix.AppendLiteral(" ) "); + } + surfacePathPrefix.AppendLiteral("]"); + + surfacePathPrefix.AppendLiteral(")/"); + + ReportValues(aHandleReport, aData, surfacePathPrefix, counter.Values()); + } + } + + static void ReportTotal(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aExplicit, + const char* aPathPrefix, const char* aPathInfix, + const MemoryTotal& aTotal) { + nsAutoCString pathPrefix; + if (aExplicit) { + pathPrefix.AppendLiteral("explicit/"); + } + pathPrefix.Append(aPathPrefix); + + nsAutoCString rasterUsedPrefix(pathPrefix); + rasterUsedPrefix.AppendLiteral("/raster/used/"); + rasterUsedPrefix.Append(aPathInfix); + ReportValues(aHandleReport, aData, rasterUsedPrefix, aTotal.UsedRaster()); + + nsAutoCString rasterUnusedPrefix(pathPrefix); + rasterUnusedPrefix.AppendLiteral("/raster/unused/"); + rasterUnusedPrefix.Append(aPathInfix); + ReportValues(aHandleReport, aData, rasterUnusedPrefix, + aTotal.UnusedRaster()); + + nsAutoCString vectorUsedPrefix(pathPrefix); + vectorUsedPrefix.AppendLiteral("/vector/used/"); + vectorUsedPrefix.Append(aPathInfix); + ReportValues(aHandleReport, aData, vectorUsedPrefix, aTotal.UsedVector()); + + nsAutoCString vectorUnusedPrefix(pathPrefix); + vectorUnusedPrefix.AppendLiteral("/vector/unused/"); + vectorUnusedPrefix.Append(aPathInfix); + ReportValues(aHandleReport, aData, vectorUnusedPrefix, + aTotal.UnusedVector()); + } + + static void ReportValues(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, const nsACString& aPathPrefix, + const MemoryCounter& aCounter) { + ReportSourceValue(aHandleReport, aData, aPathPrefix, aCounter); + + ReportValue(aHandleReport, aData, KIND_HEAP, aPathPrefix, "decoded-heap", + "Decoded image data which is stored on the heap.", + aCounter.DecodedHeap()); + + ReportValue(aHandleReport, aData, KIND_NONHEAP, aPathPrefix, + "decoded-nonheap", + "Decoded image data which isn't stored on the heap.", + aCounter.DecodedNonHeap()); + + // We don't know for certain whether or not it is on the heap, so let's + // just report it as non-heap for reporting purposes. + ReportValue(aHandleReport, aData, KIND_NONHEAP, aPathPrefix, + "decoded-unknown", + "Decoded image data which is unknown to be on the heap or not.", + aCounter.DecodedUnknown()); + } + + static void ReportSourceValue(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, + const nsACString& aPathPrefix, + const MemoryCounter& aCounter) { + ReportValue(aHandleReport, aData, KIND_HEAP, aPathPrefix, "source", + "Raster image source data and vector image documents.", + aCounter.Source()); + } + + static void ReportValue(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, int32_t aKind, + const nsACString& aPathPrefix, + const char* aPathSuffix, const char* aDescription, + size_t aValue) { + if (aValue == 0) { + return; + } + + nsAutoCString desc(aDescription); + nsAutoCString path(aPathPrefix); + path.Append(aPathSuffix); + + aHandleReport->Callback(""_ns, path, aKind, UNITS_BYTES, aValue, desc, + aData); + } + + static void RecordCounterForRequest(imgRequest* aRequest, + nsTArray* aArray, + bool aIsUsed) { + SizeOfState state(ImagesMallocSizeOf); + RefPtr image = aRequest->GetImage(); + if (image) { + ImageMemoryCounter counter(aRequest, image, state, aIsUsed); + aArray->AppendElement(std::move(counter)); + } else { + // We can at least record some information about the image from the + // request, and mark it as not knowing the image type yet. + ImageMemoryCounter counter(aRequest, state, aIsUsed); + aArray->AppendElement(std::move(counter)); + } + } +}; + +NS_IMPL_ISUPPORTS(imgMemoryReporter, nsIMemoryReporter) + +NS_IMPL_ISUPPORTS(nsProgressNotificationProxy, nsIProgressEventSink, + nsIChannelEventSink, nsIInterfaceRequestor) + +NS_IMETHODIMP +nsProgressNotificationProxy::OnProgress(nsIRequest* request, int64_t progress, + int64_t progressMax) { + nsCOMPtr loadGroup; + request->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr target; + NS_QueryNotificationCallbacks(mOriginalCallbacks, loadGroup, + NS_GET_IID(nsIProgressEventSink), + getter_AddRefs(target)); + if (!target) { + return NS_OK; + } + return target->OnProgress(mImageRequest, progress, progressMax); +} + +NS_IMETHODIMP +nsProgressNotificationProxy::OnStatus(nsIRequest* request, nsresult status, + const char16_t* statusArg) { + nsCOMPtr loadGroup; + request->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr target; + NS_QueryNotificationCallbacks(mOriginalCallbacks, loadGroup, + NS_GET_IID(nsIProgressEventSink), + getter_AddRefs(target)); + if (!target) { + return NS_OK; + } + return target->OnStatus(mImageRequest, status, statusArg); +} + +NS_IMETHODIMP +nsProgressNotificationProxy::AsyncOnChannelRedirect( + nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* cb) { + // Tell the original original callbacks about it too + nsCOMPtr loadGroup; + newChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + nsCOMPtr target; + NS_QueryNotificationCallbacks(mOriginalCallbacks, loadGroup, + NS_GET_IID(nsIChannelEventSink), + getter_AddRefs(target)); + if (!target) { + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + // Delegate to |target| if set, reusing |cb| + return target->AsyncOnChannelRedirect(oldChannel, newChannel, flags, cb); +} + +NS_IMETHODIMP +nsProgressNotificationProxy::GetInterface(const nsIID& iid, void** result) { + if (iid.Equals(NS_GET_IID(nsIProgressEventSink))) { + *result = static_cast(this); + NS_ADDREF_THIS(); + return NS_OK; + } + if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) { + *result = static_cast(this); + NS_ADDREF_THIS(); + return NS_OK; + } + if (mOriginalCallbacks) { + return mOriginalCallbacks->GetInterface(iid, result); + } + return NS_NOINTERFACE; +} + +static void NewRequestAndEntry(bool aForcePrincipalCheckForCacheEntry, + imgLoader* aLoader, const ImageCacheKey& aKey, + imgRequest** aRequest, imgCacheEntry** aEntry) { + RefPtr request = new imgRequest(aLoader, aKey); + RefPtr entry = + new imgCacheEntry(aLoader, request, aForcePrincipalCheckForCacheEntry); + aLoader->AddToUncachedImages(request); + request.forget(aRequest); + entry.forget(aEntry); +} + +static bool ShouldRevalidateEntry(imgCacheEntry* aEntry, nsLoadFlags aFlags, + bool aHasExpired) { + if (aFlags & nsIRequest::LOAD_BYPASS_CACHE) { + return false; + } + if (aFlags & nsIRequest::VALIDATE_ALWAYS) { + return true; + } + if (aEntry->GetMustValidate()) { + return true; + } + if (aHasExpired) { + // The cache entry has expired... Determine whether the stale cache + // entry can be used without validation... + if (aFlags & (nsIRequest::LOAD_FROM_CACHE | nsIRequest::VALIDATE_NEVER | + nsIRequest::VALIDATE_ONCE_PER_SESSION)) { + // LOAD_FROM_CACHE, VALIDATE_NEVER and VALIDATE_ONCE_PER_SESSION allow + // stale cache entries to be used unless they have been explicitly marked + // to indicate that revalidation is necessary. + return false; + } + // Entry is expired, revalidate. + return true; + } + return false; +} + +/* Call content policies on cached images that went through a redirect */ +static bool ShouldLoadCachedImage(imgRequest* aImgRequest, + Document* aLoadingDocument, + nsIPrincipal* aTriggeringPrincipal, + nsContentPolicyType aPolicyType, + bool aSendCSPViolationReports) { + /* Call content policies on cached images - Bug 1082837 + * Cached images are keyed off of the first uri in a redirect chain. + * Hence content policies don't get a chance to test the intermediate hops + * or the final destination. Here we test the final destination using + * mFinalURI off of the imgRequest and passing it into content policies. + * For Mixed Content Blocker, we do an additional check to determine if any + * of the intermediary hops went through an insecure redirect with the + * mHadInsecureRedirect flag + */ + bool insecureRedirect = aImgRequest->HadInsecureRedirect(); + nsCOMPtr contentLocation; + aImgRequest->GetFinalURI(getter_AddRefs(contentLocation)); + nsresult rv; + + nsCOMPtr loadingPrincipal = + aLoadingDocument ? aLoadingDocument->NodePrincipal() + : aTriggeringPrincipal; + // If there is no context and also no triggeringPrincipal, then we use a fresh + // nullPrincipal as the loadingPrincipal because we can not create a loadinfo + // without a valid loadingPrincipal. + if (!loadingPrincipal) { + loadingPrincipal = NullPrincipal::CreateWithoutOriginAttributes(); + } + + nsCOMPtr secCheckLoadInfo = new LoadInfo( + loadingPrincipal, aTriggeringPrincipal, aLoadingDocument, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, aPolicyType); + + secCheckLoadInfo->SetSendCSPViolationEvents(aSendCSPViolationReports); + + int16_t decision = nsIContentPolicy::REJECT_REQUEST; + rv = NS_CheckContentLoadPolicy(contentLocation, secCheckLoadInfo, + ""_ns, // mime guess + &decision, nsContentUtils::GetContentPolicy()); + if (NS_FAILED(rv) || !NS_CP_ACCEPTED(decision)) { + return false; + } + + // We call all Content Policies above, but we also have to call mcb + // individually to check the intermediary redirect hops are secure. + if (insecureRedirect) { + // Bug 1314356: If the image ended up in the cache upgraded by HSTS and the + // page uses upgrade-inscure-requests it had an insecure redirect + // (http->https). We need to invalidate the image and reload it because + // mixed content blocker only bails if upgrade-insecure-requests is set on + // the doc and the resource load is http: which would result in an incorrect + // mixed content warning. + nsCOMPtr docShell = + NS_CP_GetDocShellFromContext(ToSupports(aLoadingDocument)); + if (docShell) { + Document* document = docShell->GetDocument(); + if (document && document->GetUpgradeInsecureRequests(false)) { + return false; + } + } + + if (!aTriggeringPrincipal || !aTriggeringPrincipal->IsSystemPrincipal()) { + // reset the decision for mixed content blocker check + decision = nsIContentPolicy::REJECT_REQUEST; + rv = nsMixedContentBlocker::ShouldLoad(insecureRedirect, contentLocation, + secCheckLoadInfo, + ""_ns, // mime guess + true, // aReportError + &decision); + if (NS_FAILED(rv) || !NS_CP_ACCEPTED(decision)) { + return false; + } + } + } + + return true; +} + +// Returns true if this request is compatible with the given CORS mode on the +// given loading principal, and false if the request may not be reused due +// to CORS. +static bool ValidateCORSMode(imgRequest* aRequest, bool aForcePrincipalCheck, + CORSMode aCORSMode, + nsIPrincipal* aTriggeringPrincipal) { + // If the entry's CORS mode doesn't match, or the CORS mode matches but the + // document principal isn't the same, we can't use this request. + if (aRequest->GetCORSMode() != aCORSMode) { + return false; + } + + if (aRequest->GetCORSMode() != CORS_NONE || aForcePrincipalCheck) { + nsCOMPtr otherprincipal = aRequest->GetTriggeringPrincipal(); + + // If we previously had a principal, but we don't now, we can't use this + // request. + if (otherprincipal && !aTriggeringPrincipal) { + return false; + } + + if (otherprincipal && aTriggeringPrincipal && + !otherprincipal->Equals(aTriggeringPrincipal)) { + return false; + } + } + + return true; +} + +static bool ValidateSecurityInfo(imgRequest* aRequest, + bool aForcePrincipalCheck, CORSMode aCORSMode, + nsIPrincipal* aTriggeringPrincipal, + Document* aLoadingDocument, + nsContentPolicyType aPolicyType) { + if (!ValidateCORSMode(aRequest, aForcePrincipalCheck, aCORSMode, + aTriggeringPrincipal)) { + return false; + } + // Content Policy Check on Cached Images + return ShouldLoadCachedImage(aRequest, aLoadingDocument, aTriggeringPrincipal, + aPolicyType, + /* aSendCSPViolationReports */ false); +} + +static nsresult NewImageChannel( + nsIChannel** aResult, + // If aForcePrincipalCheckForCacheEntry is true, then we will + // force a principal check even when not using CORS before + // assuming we have a cache hit on a cache entry that we + // create for this channel. This is an out param that should + // be set to true if this channel ends up depending on + // aTriggeringPrincipal and false otherwise. + bool* aForcePrincipalCheckForCacheEntry, nsIURI* aURI, + nsIURI* aInitialDocumentURI, CORSMode aCORSMode, + nsIReferrerInfo* aReferrerInfo, nsILoadGroup* aLoadGroup, + nsLoadFlags aLoadFlags, nsContentPolicyType aPolicyType, + nsIPrincipal* aTriggeringPrincipal, nsINode* aRequestingNode, + bool aRespectPrivacy, uint64_t aEarlyHintPreloaderId) { + MOZ_ASSERT(aResult); + + nsresult rv; + nsCOMPtr newHttpChannel; + + nsCOMPtr callbacks; + + if (aLoadGroup) { + // Get the notification callbacks from the load group for the new channel. + // + // XXX: This is not exactly correct, because the network request could be + // referenced by multiple windows... However, the new channel needs + // something. So, using the 'first' notification callbacks is better + // than nothing... + // + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + } + + // Pass in a nullptr loadgroup because this is the underlying network + // request. This request may be referenced by several proxy image requests + // (possibly in different documents). + // If all of the proxy requests are canceled then this request should be + // canceled too. + // + + nsSecurityFlags securityFlags = + nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_INHERITED_CONTEXT); + + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + // Note we are calling NS_NewChannelWithTriggeringPrincipal() here with a + // node and a principal. This is for things like background images that are + // specified by user stylesheets, where the document is being styled, but + // the principal is that of the user stylesheet. + if (aRequestingNode && aTriggeringPrincipal) { + rv = NS_NewChannelWithTriggeringPrincipal(aResult, aURI, aRequestingNode, + aTriggeringPrincipal, + securityFlags, aPolicyType, + nullptr, // PerformanceStorage + nullptr, // loadGroup + callbacks, aLoadFlags); + + if (NS_FAILED(rv)) { + return rv; + } + + if (aPolicyType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON) { + // If this is a favicon loading, we will use the originAttributes from the + // triggeringPrincipal as the channel's originAttributes. This allows the + // favicon loading from XUL will use the correct originAttributes. + + nsCOMPtr loadInfo = (*aResult)->LoadInfo(); + rv = loadInfo->SetOriginAttributes( + aTriggeringPrincipal->OriginAttributesRef()); + } + } else { + // either we are loading something inside a document, in which case + // we should always have a requestingNode, or we are loading something + // outside a document, in which case the triggeringPrincipal and + // triggeringPrincipal should always be the systemPrincipal. + // However, there are exceptions: one is Notifications which create a + // channel in the parent process in which case we can't get a + // requestingNode. + rv = NS_NewChannel(aResult, aURI, nsContentUtils::GetSystemPrincipal(), + securityFlags, aPolicyType, + nullptr, // nsICookieJarSettings + nullptr, // PerformanceStorage + nullptr, // loadGroup + callbacks, aLoadFlags); + + if (NS_FAILED(rv)) { + return rv; + } + + // Use the OriginAttributes from the loading principal, if one is available, + // and adjust the private browsing ID based on what kind of load the caller + // has asked us to perform. + OriginAttributes attrs; + if (aTriggeringPrincipal) { + attrs = aTriggeringPrincipal->OriginAttributesRef(); + } + attrs.mPrivateBrowsingId = aRespectPrivacy ? 1 : 0; + + nsCOMPtr loadInfo = (*aResult)->LoadInfo(); + rv = loadInfo->SetOriginAttributes(attrs); + } + + if (NS_FAILED(rv)) { + return rv; + } + + // only inherit if we have a principal + *aForcePrincipalCheckForCacheEntry = + aTriggeringPrincipal && nsContentUtils::ChannelShouldInheritPrincipal( + aTriggeringPrincipal, aURI, + /* aInheritForAboutBlank */ false, + /* aForceInherit */ false); + + // Initialize HTTP-specific attributes + newHttpChannel = do_QueryInterface(*aResult); + if (newHttpChannel) { + nsCOMPtr httpChannelInternal = + do_QueryInterface(newHttpChannel); + NS_ENSURE_TRUE(httpChannelInternal, NS_ERROR_UNEXPECTED); + rv = httpChannelInternal->SetDocumentURI(aInitialDocumentURI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (aReferrerInfo) { + DebugOnly rv = newHttpChannel->SetReferrerInfo(aReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (aEarlyHintPreloaderId) { + rv = httpChannelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Image channels are loaded by default with reduced priority. + nsCOMPtr p = do_QueryInterface(*aResult); + if (p) { + uint32_t priority = nsISupportsPriority::PRIORITY_LOW; + + if (aLoadFlags & nsIRequest::LOAD_BACKGROUND) { + ++priority; // further reduce priority for background loads + } + + p->AdjustPriority(priority); + } + + // Create a new loadgroup for this new channel, using the old group as + // the parent. The indirection keeps the channel insulated from cancels, + // but does allow a way for this revalidation to be associated with at + // least one base load group for scheduling/caching purposes. + + nsCOMPtr loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + nsCOMPtr childLoadGroup = do_QueryInterface(loadGroup); + if (childLoadGroup) { + childLoadGroup->SetParentLoadGroup(aLoadGroup); + } + (*aResult)->SetLoadGroup(loadGroup); + + return NS_OK; +} + +static uint32_t SecondsFromPRTime(PRTime aTime) { + return nsContentUtils::SecondsFromPRTime(aTime); +} + +/* static */ +imgCacheEntry::imgCacheEntry(imgLoader* loader, imgRequest* request, + bool forcePrincipalCheck) + : mLoader(loader), + mRequest(request), + mDataSize(0), + mTouchedTime(SecondsFromPRTime(PR_Now())), + mLoadTime(SecondsFromPRTime(PR_Now())), + mExpiryTime(0), + mMustValidate(false), + // We start off as evicted so we don't try to update the cache. + // PutIntoCache will set this to false. + mEvicted(true), + mHasNoProxies(true), + mForcePrincipalCheck(forcePrincipalCheck), + mHasNotified(false) {} + +imgCacheEntry::~imgCacheEntry() { + LOG_FUNC(gImgLog, "imgCacheEntry::~imgCacheEntry()"); +} + +void imgCacheEntry::Touch(bool updateTime /* = true */) { + LOG_SCOPE(gImgLog, "imgCacheEntry::Touch"); + + if (updateTime) { + mTouchedTime = SecondsFromPRTime(PR_Now()); + } + + UpdateCache(); +} + +void imgCacheEntry::UpdateCache(int32_t diff /* = 0 */) { + // Don't update the cache if we've been removed from it or it doesn't care + // about our size or usage. + if (!Evicted() && HasNoProxies()) { + mLoader->CacheEntriesChanged(diff); + } +} + +void imgCacheEntry::UpdateLoadTime() { + mLoadTime = SecondsFromPRTime(PR_Now()); +} + +void imgCacheEntry::SetHasNoProxies(bool hasNoProxies) { + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + if (hasNoProxies) { + LOG_FUNC_WITH_PARAM(gImgLog, "imgCacheEntry::SetHasNoProxies true", "uri", + mRequest->CacheKey().URI()); + } else { + LOG_FUNC_WITH_PARAM(gImgLog, "imgCacheEntry::SetHasNoProxies false", + "uri", mRequest->CacheKey().URI()); + } + } + + mHasNoProxies = hasNoProxies; +} + +imgCacheQueue::imgCacheQueue() : mDirty(false), mSize(0) {} + +void imgCacheQueue::UpdateSize(int32_t diff) { mSize += diff; } + +uint32_t imgCacheQueue::GetSize() const { return mSize; } + +void imgCacheQueue::Remove(imgCacheEntry* entry) { + uint64_t index = mQueue.IndexOf(entry); + if (index == queueContainer::NoIndex) { + return; + } + + mSize -= mQueue[index]->GetDataSize(); + + // If the queue is clean and this is the first entry, + // then we can efficiently remove the entry without + // dirtying the sort order. + if (!IsDirty() && index == 0) { + std::pop_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); + mQueue.RemoveLastElement(); + return; + } + + // Remove from the middle of the list. This potentially + // breaks the binary heap sort order. + mQueue.RemoveElementAt(index); + + // If we only have one entry or the queue is empty, though, + // then the sort order is still effectively good. Simply + // refresh the list to clear the dirty flag. + if (mQueue.Length() <= 1) { + Refresh(); + return; + } + + // Otherwise we must mark the queue dirty and potentially + // trigger an expensive sort later. + MarkDirty(); +} + +void imgCacheQueue::Push(imgCacheEntry* entry) { + mSize += entry->GetDataSize(); + + RefPtr refptr(entry); + mQueue.AppendElement(std::move(refptr)); + // If we're not dirty already, then we can efficiently add this to the + // binary heap immediately. This is only O(log n). + if (!IsDirty()) { + std::push_heap(mQueue.begin(), mQueue.end(), + imgLoader::CompareCacheEntries); + } +} + +already_AddRefed imgCacheQueue::Pop() { + if (mQueue.IsEmpty()) { + return nullptr; + } + if (IsDirty()) { + Refresh(); + } + + std::pop_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); + RefPtr entry = mQueue.PopLastElement(); + + mSize -= entry->GetDataSize(); + return entry.forget(); +} + +void imgCacheQueue::Refresh() { + // Resort the list. This is an O(3 * n) operation and best avoided + // if possible. + std::make_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); + mDirty = false; +} + +void imgCacheQueue::MarkDirty() { mDirty = true; } + +bool imgCacheQueue::IsDirty() { return mDirty; } + +uint32_t imgCacheQueue::GetNumElements() const { return mQueue.Length(); } + +bool imgCacheQueue::Contains(imgCacheEntry* aEntry) const { + return mQueue.Contains(aEntry); +} + +imgCacheQueue::iterator imgCacheQueue::begin() { return mQueue.begin(); } + +imgCacheQueue::const_iterator imgCacheQueue::begin() const { + return mQueue.begin(); +} + +imgCacheQueue::iterator imgCacheQueue::end() { return mQueue.end(); } + +imgCacheQueue::const_iterator imgCacheQueue::end() const { + return mQueue.end(); +} + +nsresult imgLoader::CreateNewProxyForRequest( + imgRequest* aRequest, nsIURI* aURI, nsILoadGroup* aLoadGroup, + Document* aLoadingDocument, imgINotificationObserver* aObserver, + nsLoadFlags aLoadFlags, imgRequestProxy** _retval) { + LOG_SCOPE_WITH_PARAM(gImgLog, "imgLoader::CreateNewProxyForRequest", + "imgRequest", aRequest); + + /* XXX If we move decoding onto separate threads, we should save off the + calling thread here and pass it off to |proxyRequest| so that it call + proxy calls to |aObserver|. + */ + + RefPtr proxyRequest = new imgRequestProxy(); + + /* It is important to call |SetLoadFlags()| before calling |Init()| because + |Init()| adds the request to the loadgroup. + */ + proxyRequest->SetLoadFlags(aLoadFlags); + + // init adds itself to imgRequest's list of observers + nsresult rv = proxyRequest->Init(aRequest, aLoadGroup, aLoadingDocument, aURI, + aObserver); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + proxyRequest.forget(_retval); + return NS_OK; +} + +class imgCacheExpirationTracker final + : public nsExpirationTracker { + enum { TIMEOUT_SECONDS = 10 }; + + public: + imgCacheExpirationTracker(); + + protected: + void NotifyExpired(imgCacheEntry* entry) override; +}; + +imgCacheExpirationTracker::imgCacheExpirationTracker() + : nsExpirationTracker(TIMEOUT_SECONDS * 1000, + "imgCacheExpirationTracker") {} + +void imgCacheExpirationTracker::NotifyExpired(imgCacheEntry* entry) { + // Hold on to a reference to this entry, because the expiration tracker + // mechanism doesn't. + RefPtr kungFuDeathGrip(entry); + + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + RefPtr req = entry->GetRequest(); + if (req) { + LOG_FUNC_WITH_PARAM(gImgLog, "imgCacheExpirationTracker::NotifyExpired", + "entry", req->CacheKey().URI()); + } + } + + // We can be called multiple times on the same entry. Don't do work multiple + // times. + if (!entry->Evicted()) { + entry->Loader()->RemoveFromCache(entry); + } + + entry->Loader()->VerifyCacheSizes(); +} + +/////////////////////////////////////////////////////////////////////////////// +// imgLoader +/////////////////////////////////////////////////////////////////////////////// + +double imgLoader::sCacheTimeWeight; +uint32_t imgLoader::sCacheMaxSize; +imgMemoryReporter* imgLoader::sMemReporter; + +NS_IMPL_ISUPPORTS(imgLoader, imgILoader, nsIContentSniffer, imgICache, + nsISupportsWeakReference, nsIObserver) + +static imgLoader* gNormalLoader = nullptr; +static imgLoader* gPrivateBrowsingLoader = nullptr; + +/* static */ +already_AddRefed imgLoader::CreateImageLoader() { + // In some cases, such as xpctests, XPCOM modules are not automatically + // initialized. We need to make sure that our module is initialized before + // we hand out imgLoader instances and code starts using them. + mozilla::image::EnsureModuleInitialized(); + + RefPtr loader = new imgLoader(); + loader->Init(); + + return loader.forget(); +} + +imgLoader* imgLoader::NormalLoader() { + if (!gNormalLoader) { + gNormalLoader = CreateImageLoader().take(); + } + return gNormalLoader; +} + +imgLoader* imgLoader::PrivateBrowsingLoader() { + if (!gPrivateBrowsingLoader) { + gPrivateBrowsingLoader = CreateImageLoader().take(); + gPrivateBrowsingLoader->RespectPrivacyNotifications(); + } + return gPrivateBrowsingLoader; +} + +imgLoader::imgLoader() + : mUncachedImagesMutex("imgLoader::UncachedImages"), + mRespectPrivacy(false) { + sMemReporter->AddRef(); + sMemReporter->RegisterLoader(this); +} + +imgLoader::~imgLoader() { + ClearImageCache(); + { + // If there are any of our imgRequest's left they are in the uncached + // images set, so clear their pointer to us. + MutexAutoLock lock(mUncachedImagesMutex); + for (RefPtr req : mUncachedImages) { + req->ClearLoader(); + } + } + sMemReporter->UnregisterLoader(this); + sMemReporter->Release(); +} + +void imgLoader::VerifyCacheSizes() { +#ifdef DEBUG + if (!mCacheTracker) { + return; + } + + uint32_t cachesize = mCache.Count(); + uint32_t queuesize = mCacheQueue.GetNumElements(); + uint32_t trackersize = 0; + for (nsExpirationTracker::Iterator it(mCacheTracker.get()); + it.Next();) { + trackersize++; + } + MOZ_ASSERT(queuesize == trackersize, "Queue and tracker sizes out of sync!"); + MOZ_ASSERT(queuesize <= cachesize, "Queue has more elements than cache!"); +#endif +} + +void imgLoader::GlobalInit() { + sCacheTimeWeight = StaticPrefs::image_cache_timeweight_AtStartup() / 1000.0; + int32_t cachesize = StaticPrefs::image_cache_size_AtStartup(); + sCacheMaxSize = cachesize > 0 ? cachesize : 0; + + sMemReporter = new imgMemoryReporter(); + RegisterStrongAsyncMemoryReporter(sMemReporter); + RegisterImagesContentUsedUncompressedDistinguishedAmount( + imgMemoryReporter::ImagesContentUsedUncompressedDistinguishedAmount); +} + +void imgLoader::ShutdownMemoryReporter() { + UnregisterImagesContentUsedUncompressedDistinguishedAmount(); + UnregisterStrongMemoryReporter(sMemReporter); +} + +nsresult imgLoader::InitCache() { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (!os) { + return NS_ERROR_FAILURE; + } + + os->AddObserver(this, "memory-pressure", false); + os->AddObserver(this, "chrome-flush-caches", false); + os->AddObserver(this, "last-pb-context-exited", false); + os->AddObserver(this, "profile-before-change", false); + os->AddObserver(this, "xpcom-shutdown", false); + + mCacheTracker = MakeUnique(); + + return NS_OK; +} + +nsresult imgLoader::Init() { + InitCache(); + + return NS_OK; +} + +NS_IMETHODIMP +imgLoader::RespectPrivacyNotifications() { + mRespectPrivacy = true; + return NS_OK; +} + +NS_IMETHODIMP +imgLoader::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, "memory-pressure") == 0) { + MinimizeCache(); + } else if (strcmp(aTopic, "chrome-flush-caches") == 0) { + MinimizeCache(); + ClearImageCache({ClearOption::ChromeOnly}); + } else if (strcmp(aTopic, "last-pb-context-exited") == 0) { + if (mRespectPrivacy) { + ClearImageCache(); + } + } else if (strcmp(aTopic, "profile-before-change") == 0) { + mCacheTracker = nullptr; + } else if (strcmp(aTopic, "xpcom-shutdown") == 0) { + mCacheTracker = nullptr; + ShutdownMemoryReporter(); + + } else { + // (Nothing else should bring us here) + MOZ_ASSERT(0, "Invalid topic received"); + } + + return NS_OK; +} + +NS_IMETHODIMP +imgLoader::ClearCache(bool chrome) { + if (XRE_IsParentProcess()) { + bool privateLoader = this == gPrivateBrowsingLoader; + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendClearImageCache(privateLoader, chrome); + } + } + ClearOptions options; + if (chrome) { + options += ClearOption::ChromeOnly; + } + return ClearImageCache(options); +} + +NS_IMETHODIMP +imgLoader::RemoveEntriesFromPrincipalInAllProcesses(nsIPrincipal* aPrincipal) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendClearImageCacheFromPrincipal(aPrincipal); + } + + imgLoader* loader; + if (aPrincipal->OriginAttributesRef().mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + loader = imgLoader::NormalLoader(); + } else { + loader = imgLoader::PrivateBrowsingLoader(); + } + + return loader->RemoveEntriesInternal(aPrincipal, nullptr); +} + +NS_IMETHODIMP +imgLoader::RemoveEntriesFromBaseDomainInAllProcesses( + const nsACString& aBaseDomain) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendClearImageCacheFromBaseDomain(aBaseDomain); + } + + return RemoveEntriesInternal(nullptr, &aBaseDomain); +} + +nsresult imgLoader::RemoveEntriesInternal(nsIPrincipal* aPrincipal, + const nsACString* aBaseDomain) { + // Can only clear by either principal or base domain. + if ((!aPrincipal && !aBaseDomain) || (aPrincipal && aBaseDomain)) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoString origin; + if (aPrincipal) { + nsresult rv = nsContentUtils::GetUTFOrigin(aPrincipal, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsCOMPtr tldService; + AutoTArray, 128> entriesToBeRemoved; + + // For base domain we only clear the non-chrome cache. + for (const auto& entry : mCache) { + const auto& key = entry.GetKey(); + + const bool shouldRemove = [&] { + if (aPrincipal) { + if (key.OriginAttributesRef() != + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef()) { + return false; + } + + nsAutoString imageOrigin; + nsresult rv = nsContentUtils::GetUTFOrigin(key.URI(), imageOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return imageOrigin == origin; + } + + if (!aBaseDomain) { + return false; + } + // Clear by baseDomain. + nsAutoCString host; + nsresult rv = key.URI()->GetHost(host); + if (NS_FAILED(rv) || host.IsEmpty()) { + return false; + } + + if (!tldService) { + tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + } + if (NS_WARN_IF(!tldService)) { + return false; + } + + bool hasRootDomain = false; + rv = tldService->HasRootDomain(host, *aBaseDomain, &hasRootDomain); + if (NS_SUCCEEDED(rv) && hasRootDomain) { + return true; + } + + // If we don't get a direct base domain match, also check for cache of + // third parties partitioned under aBaseDomain. + + // The isolation key is either just the base domain, or an origin suffix + // which contains the partitionKey holding the baseDomain. + + if (key.IsolationKeyRef().Equals(*aBaseDomain)) { + return true; + } + + // The isolation key does not match the given base domain. It may be an + // origin suffix. Parse it into origin attributes. + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(key.IsolationKeyRef())) { + // Key is not an origin suffix. + return false; + } + + return StoragePrincipalHelper::PartitionKeyHasBaseDomain( + attrs.mPartitionKey, *aBaseDomain); + }(); + + if (shouldRemove) { + entriesToBeRemoved.AppendElement(entry.GetData()); + } + } + + for (auto& entry : entriesToBeRemoved) { + if (!RemoveFromCache(entry)) { + NS_WARNING( + "Couldn't remove an entry from the cache in " + "RemoveEntriesInternal()\n"); + } + } + + return NS_OK; +} + +constexpr auto AllCORSModes() { + return MakeInclusiveEnumeratedRange(kFirstCORSMode, kLastCORSMode); +} + +NS_IMETHODIMP +imgLoader::RemoveEntry(nsIURI* aURI, Document* aDoc) { + if (!aURI) { + return NS_OK; + } + OriginAttributes attrs; + if (aDoc) { + attrs = aDoc->NodePrincipal()->OriginAttributesRef(); + } + for (auto corsMode : AllCORSModes()) { + ImageCacheKey key(aURI, corsMode, attrs, aDoc); + RemoveFromCache(key); + } + return NS_OK; +} + +NS_IMETHODIMP +imgLoader::FindEntryProperties(nsIURI* uri, Document* aDoc, + nsIProperties** _retval) { + *_retval = nullptr; + + OriginAttributes attrs; + if (aDoc) { + nsCOMPtr principal = aDoc->NodePrincipal(); + if (principal) { + attrs = principal->OriginAttributesRef(); + } + } + + for (auto corsMode : AllCORSModes()) { + ImageCacheKey key(uri, corsMode, attrs, aDoc); + RefPtr entry; + if (!mCache.Get(key, getter_AddRefs(entry)) || !entry) { + continue; + } + if (mCacheTracker && entry->HasNoProxies()) { + mCacheTracker->MarkUsed(entry); + } + RefPtr request = entry->GetRequest(); + if (request) { + nsCOMPtr properties = request->Properties(); + properties.forget(_retval); + return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP_(void) +imgLoader::ClearCacheForControlledDocument(Document* aDoc) { + MOZ_ASSERT(aDoc); + AutoTArray, 128> entriesToBeRemoved; + for (const auto& entry : mCache) { + const auto& key = entry.GetKey(); + if (key.ControlledDocument() == aDoc) { + entriesToBeRemoved.AppendElement(entry.GetData()); + } + } + for (auto& entry : entriesToBeRemoved) { + if (!RemoveFromCache(entry)) { + NS_WARNING( + "Couldn't remove an entry from the cache in " + "ClearCacheForControlledDocument()\n"); + } + } +} + +void imgLoader::Shutdown() { + NS_IF_RELEASE(gNormalLoader); + gNormalLoader = nullptr; + NS_IF_RELEASE(gPrivateBrowsingLoader); + gPrivateBrowsingLoader = nullptr; +} + +bool imgLoader::PutIntoCache(const ImageCacheKey& aKey, imgCacheEntry* entry) { + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::PutIntoCache", "uri", + aKey.URI()); + + // Check to see if this request already exists in the cache. If so, we'll + // replace the old version. + RefPtr tmpCacheEntry; + if (mCache.Get(aKey, getter_AddRefs(tmpCacheEntry)) && tmpCacheEntry) { + MOZ_LOG( + gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::PutIntoCache -- Element already in the cache", + nullptr)); + RefPtr tmpRequest = tmpCacheEntry->GetRequest(); + + // If it already exists, and we're putting the same key into the cache, we + // should remove the old version. + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::PutIntoCache -- Replacing cached element", + nullptr)); + + RemoveFromCache(aKey); + } else { + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::PutIntoCache --" + " Element NOT already in the cache", + nullptr)); + } + + mCache.InsertOrUpdate(aKey, RefPtr{entry}); + + // We can be called to resurrect an evicted entry. + if (entry->Evicted()) { + entry->SetEvicted(false); + } + + // If we're resurrecting an entry with no proxies, put it back in the + // tracker and queue. + if (entry->HasNoProxies()) { + nsresult addrv = NS_OK; + + if (mCacheTracker) { + addrv = mCacheTracker->AddObject(entry); + } + + if (NS_SUCCEEDED(addrv)) { + mCacheQueue.Push(entry); + } + } + + RefPtr request = entry->GetRequest(); + request->SetIsInCache(true); + RemoveFromUncachedImages(request); + + return true; +} + +bool imgLoader::SetHasNoProxies(imgRequest* aRequest, imgCacheEntry* aEntry) { + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::SetHasNoProxies", "uri", + aRequest->CacheKey().URI()); + + aEntry->SetHasNoProxies(true); + + if (aEntry->Evicted()) { + return false; + } + + nsresult addrv = NS_OK; + + if (mCacheTracker) { + addrv = mCacheTracker->AddObject(aEntry); + } + + if (NS_SUCCEEDED(addrv)) { + mCacheQueue.Push(aEntry); + } + + return true; +} + +bool imgLoader::SetHasProxies(imgRequest* aRequest) { + VerifyCacheSizes(); + + const ImageCacheKey& key = aRequest->CacheKey(); + + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::SetHasProxies", "uri", + key.URI()); + + RefPtr entry; + if (mCache.Get(key, getter_AddRefs(entry)) && entry) { + // Make sure the cache entry is for the right request + RefPtr entryRequest = entry->GetRequest(); + if (entryRequest == aRequest && entry->HasNoProxies()) { + mCacheQueue.Remove(entry); + + if (mCacheTracker) { + mCacheTracker->RemoveObject(entry); + } + + entry->SetHasNoProxies(false); + + return true; + } + } + + return false; +} + +void imgLoader::CacheEntriesChanged(int32_t aSizeDiff /* = 0 */) { + // We only need to dirty the queue if there is any sorting + // taking place. Empty or single-entry lists can't become + // dirty. + if (mCacheQueue.GetNumElements() > 1) { + mCacheQueue.MarkDirty(); + } + mCacheQueue.UpdateSize(aSizeDiff); +} + +void imgLoader::CheckCacheLimits() { + if (mCacheQueue.GetNumElements() == 0) { + NS_ASSERTION(mCacheQueue.GetSize() == 0, + "imgLoader::CheckCacheLimits -- incorrect cache size"); + } + + // Remove entries from the cache until we're back at our desired max size. + while (mCacheQueue.GetSize() > sCacheMaxSize) { + // Remove the first entry in the queue. + RefPtr entry(mCacheQueue.Pop()); + + NS_ASSERTION(entry, "imgLoader::CheckCacheLimits -- NULL entry pointer"); + + if (MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + RefPtr req = entry->GetRequest(); + if (req) { + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::CheckCacheLimits", + "entry", req->CacheKey().URI()); + } + } + + if (entry) { + // We just popped this entry from the queue, so pass AlreadyRemoved + // to avoid searching the queue again in RemoveFromCache. + RemoveFromCache(entry, QueueState::AlreadyRemoved); + } + } +} + +bool imgLoader::ValidateRequestWithNewChannel( + imgRequest* request, nsIURI* aURI, nsIURI* aInitialDocumentURI, + nsIReferrerInfo* aReferrerInfo, nsILoadGroup* aLoadGroup, + imgINotificationObserver* aObserver, Document* aLoadingDocument, + uint64_t aInnerWindowId, nsLoadFlags aLoadFlags, + nsContentPolicyType aLoadPolicyType, imgRequestProxy** aProxyRequest, + nsIPrincipal* aTriggeringPrincipal, CORSMode aCORSMode, bool aLinkPreload, + uint64_t aEarlyHintPreloaderId, bool* aNewChannelCreated) { + // now we need to insert a new channel request object in between the real + // request and the proxy that basically delays loading the image until it + // gets a 304 or figures out that this needs to be a new request + + nsresult rv; + + // If we're currently in the middle of validating this request, just hand + // back a proxy to it; the required work will be done for us. + if (imgCacheValidator* validator = request->GetValidator()) { + rv = CreateNewProxyForRequest(request, aURI, aLoadGroup, aLoadingDocument, + aObserver, aLoadFlags, aProxyRequest); + if (NS_FAILED(rv)) { + return false; + } + + if (*aProxyRequest) { + imgRequestProxy* proxy = static_cast(*aProxyRequest); + + // We will send notifications from imgCacheValidator::OnStartRequest(). + // In the mean time, we must defer notifications because we are added to + // the imgRequest's proxy list, and we can get extra notifications + // resulting from methods such as StartDecoding(). See bug 579122. + proxy->MarkValidating(); + + if (aLinkPreload) { + MOZ_ASSERT(aLoadingDocument); + proxy->PrioritizeAsPreload(); + auto preloadKey = PreloadHashKey::CreateAsImage( + aURI, aTriggeringPrincipal, aCORSMode); + proxy->NotifyOpen(preloadKey, aLoadingDocument, true); + } + + // Attach the proxy without notifying + validator->AddProxy(proxy); + } + + return true; + } + // We will rely on Necko to cache this request when it's possible, and to + // tell imgCacheValidator::OnStartRequest whether the request came from its + // cache. + nsCOMPtr newChannel; + bool forcePrincipalCheck; + rv = + NewImageChannel(getter_AddRefs(newChannel), &forcePrincipalCheck, aURI, + aInitialDocumentURI, aCORSMode, aReferrerInfo, aLoadGroup, + aLoadFlags, aLoadPolicyType, aTriggeringPrincipal, + aLoadingDocument, mRespectPrivacy, aEarlyHintPreloaderId); + if (NS_FAILED(rv)) { + return false; + } + + if (aNewChannelCreated) { + *aNewChannelCreated = true; + } + + RefPtr req; + rv = CreateNewProxyForRequest(request, aURI, aLoadGroup, aLoadingDocument, + aObserver, aLoadFlags, getter_AddRefs(req)); + if (NS_FAILED(rv)) { + return false; + } + + // Make sure that OnStatus/OnProgress calls have the right request set... + RefPtr progressproxy = + new nsProgressNotificationProxy(newChannel, req); + if (!progressproxy) { + return false; + } + + RefPtr hvc = + new imgCacheValidator(progressproxy, this, request, aLoadingDocument, + aInnerWindowId, forcePrincipalCheck); + + // Casting needed here to get past multiple inheritance. + nsCOMPtr listener = + do_QueryInterface(static_cast(hvc)); + NS_ENSURE_TRUE(listener, false); + + // We must set the notification callbacks before setting up the + // CORS listener, because that's also interested inthe + // notification callbacks. + newChannel->SetNotificationCallbacks(hvc); + + request->SetValidator(hvc); + + // We will send notifications from imgCacheValidator::OnStartRequest(). + // In the mean time, we must defer notifications because we are added to + // the imgRequest's proxy list, and we can get extra notifications + // resulting from methods such as StartDecoding(). See bug 579122. + req->MarkValidating(); + + if (aLinkPreload) { + MOZ_ASSERT(aLoadingDocument); + req->PrioritizeAsPreload(); + auto preloadKey = + PreloadHashKey::CreateAsImage(aURI, aTriggeringPrincipal, aCORSMode); + req->NotifyOpen(preloadKey, aLoadingDocument, true); + } + + // Add the proxy without notifying + hvc->AddProxy(req); + + mozilla::net::PredictorLearn(aURI, aInitialDocumentURI, + nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE, + aLoadGroup); + rv = newChannel->AsyncOpen(listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + req->CancelAndForgetObserver(rv); + // This will notify any current or future tags. Pass the + // non-open channel so that we can read loadinfo and referrer info of that + // channel. + req->NotifyStart(newChannel); + // Use the non-channel overload of this method to force the notification to + // happen. The preload request has not been assigned a channel. + req->NotifyStop(rv); + return false; + } + + req.forget(aProxyRequest); + return true; +} + +void imgLoader::NotifyObserversForCachedImage( + imgCacheEntry* aEntry, imgRequest* request, nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, Document* aLoadingDocument, + nsIPrincipal* aTriggeringPrincipal, CORSMode aCORSMode, + uint64_t aEarlyHintPreloaderId) { + if (aEntry->HasNotified()) { + return; + } + + nsCOMPtr obsService = services::GetObserverService(); + + if (!obsService->HasObservers("http-on-image-cache-response")) { + return; + } + + aEntry->SetHasNotified(); + + nsCOMPtr newChannel; + bool forcePrincipalCheck; + nsresult rv = NewImageChannel( + getter_AddRefs(newChannel), &forcePrincipalCheck, aURI, nullptr, + aCORSMode, aReferrerInfo, nullptr, 0, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, aTriggeringPrincipal, + aLoadingDocument, mRespectPrivacy, aEarlyHintPreloaderId); + if (NS_FAILED(rv)) { + return; + } + + RefPtr httpBaseChannel = do_QueryObject(newChannel); + if (httpBaseChannel) { + httpBaseChannel->SetDummyChannelForImageCache(); + newChannel->SetContentType(nsDependentCString(request->GetMimeType())); + RefPtr image = request->GetImage(); + if (image) { + newChannel->SetContentLength(aEntry->GetDataSize()); + } + obsService->NotifyObservers(newChannel, "http-on-image-cache-response", + nullptr); + } +} + +bool imgLoader::ValidateEntry( + imgCacheEntry* aEntry, nsIURI* aURI, nsIURI* aInitialDocumentURI, + nsIReferrerInfo* aReferrerInfo, nsILoadGroup* aLoadGroup, + imgINotificationObserver* aObserver, Document* aLoadingDocument, + nsLoadFlags aLoadFlags, nsContentPolicyType aLoadPolicyType, + bool aCanMakeNewChannel, bool* aNewChannelCreated, + imgRequestProxy** aProxyRequest, nsIPrincipal* aTriggeringPrincipal, + CORSMode aCORSMode, bool aLinkPreload, uint64_t aEarlyHintPreloaderId) { + LOG_SCOPE(gImgLog, "imgLoader::ValidateEntry"); + + // If the expiration time is zero, then the request has not gotten far enough + // to know when it will expire, or we know it will never expire (see + // nsContentUtils::GetSubresourceCacheValidationInfo). + uint32_t expiryTime = aEntry->GetExpiryTime(); + bool hasExpired = expiryTime && expiryTime <= SecondsFromPRTime(PR_Now()); + + nsresult rv; + + // Special treatment for file URLs - aEntry has expired if file has changed + nsCOMPtr fileUrl(do_QueryInterface(aURI)); + if (fileUrl) { + uint32_t lastModTime = aEntry->GetLoadTime(); + + nsCOMPtr theFile; + rv = fileUrl->GetFile(getter_AddRefs(theFile)); + if (NS_SUCCEEDED(rv)) { + PRTime fileLastMod; + rv = theFile->GetLastModifiedTime(&fileLastMod); + if (NS_SUCCEEDED(rv)) { + // nsIFile uses millisec, NSPR usec + fileLastMod *= 1000; + hasExpired = SecondsFromPRTime((PRTime)fileLastMod) > lastModTime; + } + } + } + + RefPtr request(aEntry->GetRequest()); + + if (!request) { + return false; + } + + if (!ValidateSecurityInfo(request, aEntry->ForcePrincipalCheck(), aCORSMode, + aTriggeringPrincipal, aLoadingDocument, + aLoadPolicyType)) { + return false; + } + + // data URIs are immutable and by their nature can't leak data, so we can + // just return true in that case. Doing so would mean that shift-reload + // doesn't reload data URI documents/images though (which is handy for + // debugging during gecko development) so we make an exception in that case. + if (aURI->SchemeIs("data") && !(aLoadFlags & nsIRequest::LOAD_BYPASS_CACHE)) { + return true; + } + + bool validateRequest = false; + + if (!request->CanReuseWithoutValidation(aLoadingDocument)) { + // If we would need to revalidate this entry, but we're being told to + // bypass the cache, we don't allow this entry to be used. + if (aLoadFlags & nsIRequest::LOAD_BYPASS_CACHE) { + return false; + } + + if (MOZ_UNLIKELY(ChaosMode::isActive(ChaosFeature::ImageCache))) { + if (ChaosMode::randomUint32LessThan(4) < 1) { + return false; + } + } + + // Determine whether the cache aEntry must be revalidated... + validateRequest = ShouldRevalidateEntry(aEntry, aLoadFlags, hasExpired); + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("imgLoader::ValidateEntry validating cache entry. " + "validateRequest = %d", + validateRequest)); + } else if (!aLoadingDocument && MOZ_LOG_TEST(gImgLog, LogLevel::Debug)) { + MOZ_LOG(gImgLog, LogLevel::Debug, + ("imgLoader::ValidateEntry BYPASSING cache validation for %s " + "because of NULL loading document", + aURI->GetSpecOrDefault().get())); + } + + // If the original request is still transferring don't kick off a validation + // network request because it is a bit silly to issue a validation request if + // the original request hasn't even finished yet. So just return true + // indicating the caller can create a new proxy for the request and use it as + // is. + // This is an optimization but it's also required for correctness. If we don't + // do this then when firing the load complete notification for the original + // request that can unblock load for the document and then spin the event loop + // (see the stack in bug 1641682) which then the OnStartRequest for the + // validation request can fire which can call UpdateProxies and can sync + // notify on the progress tracker about all existing state, which includes + // load complete, so we fire a second load complete notification for the + // image. + // In addition, we want to validate if the original request encountered + // an error for two reasons. The first being if the error was a network error + // then trying to re-fetch the image might succeed. The second is more + // complicated. We decide if we should fire the load or error event for img + // elements depending on if the image has error in its status at the time when + // the load complete notification is received, and we set error status on an + // image if it encounters a network error or a decode error with no real way + // to tell them apart. So if we load an image that will produce a decode error + // the first time we will usually fire the load event, and then decode enough + // to encounter the decode error and set the error status on the image. The + // next time we reference the image in the same document the load complete + // notification is replayed and this time the error status from the decode is + // already present so we fire the error event instead of the load event. This + // is a bug (bug 1645576) that we should fix. In order to avoid that bug in + // some cases (specifically the cases when we hit this code and try to + // validate the request) we make sure to validate. This avoids the bug because + // when the load complete notification arrives the proxy is marked as + // validating so it lies about its status and returns nothing. + bool requestComplete = false; + RefPtr tracker; + RefPtr image = request->GetImage(); + if (image) { + tracker = image->GetProgressTracker(); + } else { + tracker = request->GetProgressTracker(); + } + if (tracker) { + if (tracker->GetProgress() & (FLAG_LOAD_COMPLETE | FLAG_HAS_ERROR)) { + requestComplete = true; + } + } + if (!requestComplete) { + return true; + } + + if (validateRequest && aCanMakeNewChannel) { + LOG_SCOPE(gImgLog, "imgLoader::ValidateRequest |cache hit| must validate"); + + uint64_t innerWindowID = + aLoadingDocument ? aLoadingDocument->InnerWindowID() : 0; + return ValidateRequestWithNewChannel( + request, aURI, aInitialDocumentURI, aReferrerInfo, aLoadGroup, + aObserver, aLoadingDocument, innerWindowID, aLoadFlags, aLoadPolicyType, + aProxyRequest, aTriggeringPrincipal, aCORSMode, aLinkPreload, + aEarlyHintPreloaderId, aNewChannelCreated); + } + + if (!validateRequest) { + NotifyObserversForCachedImage(aEntry, request, aURI, aReferrerInfo, + aLoadingDocument, aTriggeringPrincipal, + aCORSMode, aEarlyHintPreloaderId); + } + + return !validateRequest; +} + +bool imgLoader::RemoveFromCache(const ImageCacheKey& aKey) { + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::RemoveFromCache", "uri", + aKey.URI()); + RefPtr entry; + mCache.Remove(aKey, getter_AddRefs(entry)); + if (entry) { + MOZ_ASSERT(!entry->Evicted(), "Evicting an already-evicted cache entry!"); + + // Entries with no proxies are in the tracker. + if (entry->HasNoProxies()) { + if (mCacheTracker) { + mCacheTracker->RemoveObject(entry); + } + mCacheQueue.Remove(entry); + } + + entry->SetEvicted(true); + + RefPtr request = entry->GetRequest(); + request->SetIsInCache(false); + AddToUncachedImages(request); + + return true; + } + return false; +} + +bool imgLoader::RemoveFromCache(imgCacheEntry* entry, QueueState aQueueState) { + LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache entry"); + + RefPtr request = entry->GetRequest(); + if (request) { + const ImageCacheKey& key = request->CacheKey(); + LOG_STATIC_FUNC_WITH_PARAM(gImgLog, "imgLoader::RemoveFromCache", + "entry's uri", key.URI()); + + mCache.Remove(key); + + if (entry->HasNoProxies()) { + LOG_STATIC_FUNC(gImgLog, + "imgLoader::RemoveFromCache removing from tracker"); + if (mCacheTracker) { + mCacheTracker->RemoveObject(entry); + } + // Only search the queue to remove the entry if its possible it might + // be in the queue. If we know its not in the queue this would be + // wasted work. + MOZ_ASSERT_IF(aQueueState == QueueState::AlreadyRemoved, + !mCacheQueue.Contains(entry)); + if (aQueueState == QueueState::MaybeExists) { + mCacheQueue.Remove(entry); + } + } + + entry->SetEvicted(true); + request->SetIsInCache(false); + AddToUncachedImages(request); + + return true; + } + + return false; +} + +nsresult imgLoader::ClearImageCache(ClearOptions aOptions) { + const bool chromeOnly = aOptions.contains(ClearOption::ChromeOnly); + const auto ShouldRemove = [&](imgCacheEntry* aEntry) { + if (chromeOnly) { + // TODO: Consider also removing "resource://" etc? + RefPtr request = aEntry->GetRequest(); + if (!request || !request->CacheKey().URI()->SchemeIs("chrome")) { + return false; + } + } + return true; + }; + if (aOptions.contains(ClearOption::UnusedOnly)) { + LOG_STATIC_FUNC(gImgLog, "imgLoader::ClearImageCache queue"); + // We have to make a temporary, since RemoveFromCache removes the element + // from the queue, invalidating iterators. + nsTArray> entries(mCacheQueue.GetNumElements()); + for (auto& entry : mCacheQueue) { + if (ShouldRemove(entry)) { + entries.AppendElement(entry); + } + } + + // Iterate in reverse order to minimize array copying. + for (auto& entry : entries) { + if (!RemoveFromCache(entry)) { + return NS_ERROR_FAILURE; + } + } + + MOZ_ASSERT(chromeOnly || mCacheQueue.GetNumElements() == 0); + return NS_OK; + } + + LOG_STATIC_FUNC(gImgLog, "imgLoader::ClearImageCache table"); + // We have to make a temporary, since RemoveFromCache removes the element + // from the queue, invalidating iterators. + const auto entries = + ToTArray>>(mCache.Values()); + for (const auto& entry : entries) { + if (!ShouldRemove(entry)) { + continue; + } + if (!RemoveFromCache(entry)) { + return NS_ERROR_FAILURE; + } + } + MOZ_ASSERT(chromeOnly || mCache.IsEmpty()); + return NS_OK; +} + +void imgLoader::AddToUncachedImages(imgRequest* aRequest) { + MutexAutoLock lock(mUncachedImagesMutex); + mUncachedImages.Insert(aRequest); +} + +void imgLoader::RemoveFromUncachedImages(imgRequest* aRequest) { + MutexAutoLock lock(mUncachedImagesMutex); + mUncachedImages.Remove(aRequest); +} + +#define LOAD_FLAGS_CACHE_MASK \ + (nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::LOAD_FROM_CACHE) + +#define LOAD_FLAGS_VALIDATE_MASK \ + (nsIRequest::VALIDATE_ALWAYS | nsIRequest::VALIDATE_NEVER | \ + nsIRequest::VALIDATE_ONCE_PER_SESSION) + +NS_IMETHODIMP +imgLoader::LoadImageXPCOM( + nsIURI* aURI, nsIURI* aInitialDocumentURI, nsIReferrerInfo* aReferrerInfo, + nsIPrincipal* aTriggeringPrincipal, nsILoadGroup* aLoadGroup, + imgINotificationObserver* aObserver, Document* aLoadingDocument, + nsLoadFlags aLoadFlags, nsISupports* aCacheKey, + nsContentPolicyType aContentPolicyType, imgIRequest** _retval) { + // Optional parameter, so defaults to 0 (== TYPE_INVALID) + if (!aContentPolicyType) { + aContentPolicyType = nsIContentPolicy::TYPE_INTERNAL_IMAGE; + } + imgRequestProxy* proxy; + nsresult rv = + LoadImage(aURI, aInitialDocumentURI, aReferrerInfo, aTriggeringPrincipal, + 0, aLoadGroup, aObserver, aLoadingDocument, aLoadingDocument, + aLoadFlags, aCacheKey, aContentPolicyType, u""_ns, + /* aUseUrgentStartForChannel */ false, /* aListPreload */ false, + 0, &proxy); + *_retval = proxy; + return rv; +} + +static void MakeRequestStaticIfNeeded( + Document* aLoadingDocument, imgRequestProxy** aProxyAboutToGetReturned) { + if (!aLoadingDocument || !aLoadingDocument->IsStaticDocument()) { + return; + } + + if (!*aProxyAboutToGetReturned) { + return; + } + + RefPtr proxy = dont_AddRef(*aProxyAboutToGetReturned); + *aProxyAboutToGetReturned = nullptr; + + RefPtr staticProxy = + proxy->GetStaticRequest(aLoadingDocument); + if (staticProxy != proxy) { + proxy->CancelAndForgetObserver(NS_BINDING_ABORTED); + proxy = std::move(staticProxy); + } + proxy.forget(aProxyAboutToGetReturned); +} + +bool imgLoader::IsImageAvailable(nsIURI* aURI, + nsIPrincipal* aTriggeringPrincipal, + CORSMode aCORSMode, Document* aDocument) { + ImageCacheKey key(aURI, aCORSMode, + aTriggeringPrincipal->OriginAttributesRef(), aDocument); + RefPtr entry; + if (!mCache.Get(key, getter_AddRefs(entry)) || !entry) { + return false; + } + RefPtr request = entry->GetRequest(); + if (!request) { + return false; + } + if (nsCOMPtr docLoadGroup = aDocument->GetDocumentLoadGroup()) { + nsLoadFlags requestFlags = nsIRequest::LOAD_NORMAL; + docLoadGroup->GetLoadFlags(&requestFlags); + if (requestFlags & nsIRequest::LOAD_BYPASS_CACHE) { + // If we're bypassing the cache, treat the image as not available. + return false; + } + } + return ValidateCORSMode(request, false, aCORSMode, aTriggeringPrincipal); +} + +nsresult imgLoader::LoadImage( + nsIURI* aURI, nsIURI* aInitialDocumentURI, nsIReferrerInfo* aReferrerInfo, + nsIPrincipal* aTriggeringPrincipal, uint64_t aRequestContextID, + nsILoadGroup* aLoadGroup, imgINotificationObserver* aObserver, + nsINode* aContext, Document* aLoadingDocument, nsLoadFlags aLoadFlags, + nsISupports* aCacheKey, nsContentPolicyType aContentPolicyType, + const nsAString& initiatorType, bool aUseUrgentStartForChannel, + bool aLinkPreload, uint64_t aEarlyHintPreloaderId, + imgRequestProxy** _retval) { + VerifyCacheSizes(); + + NS_ASSERTION(aURI, "imgLoader::LoadImage -- NULL URI pointer"); + + if (!aURI) { + return NS_ERROR_NULL_POINTER; + } + + auto makeStaticIfNeeded = mozilla::MakeScopeExit( + [&] { MakeRequestStaticIfNeeded(aLoadingDocument, _retval); }); + + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("imgLoader::LoadImage", NETWORK, + aURI->GetSpecOrDefault()); + + LOG_SCOPE_WITH_PARAM(gImgLog, "imgLoader::LoadImage", "aURI", aURI); + + *_retval = nullptr; + + RefPtr request; + + nsresult rv; + nsLoadFlags requestFlags = nsIRequest::LOAD_NORMAL; + +#ifdef DEBUG + bool isPrivate = false; + + if (aLoadingDocument) { + isPrivate = nsContentUtils::IsInPrivateBrowsing(aLoadingDocument); + } else if (aLoadGroup) { + isPrivate = nsContentUtils::IsInPrivateBrowsing(aLoadGroup); + } + MOZ_ASSERT(isPrivate == mRespectPrivacy); + + if (aLoadingDocument) { + // The given load group should match that of the document if given. If + // that isn't the case, then we need to add more plumbing to ensure we + // block the document as well. + nsCOMPtr docLoadGroup = + aLoadingDocument->GetDocumentLoadGroup(); + MOZ_ASSERT(docLoadGroup == aLoadGroup); + } +#endif + + // Get the default load flags from the loadgroup (if possible)... + if (aLoadGroup) { + aLoadGroup->GetLoadFlags(&requestFlags); + } + // + // Merge the default load flags with those passed in via aLoadFlags. + // Currently, *only* the caching, validation and background load flags + // are merged... + // + // The flags in aLoadFlags take precedence over the default flags! + // + if (aLoadFlags & LOAD_FLAGS_CACHE_MASK) { + // Override the default caching flags... + requestFlags = (requestFlags & ~LOAD_FLAGS_CACHE_MASK) | + (aLoadFlags & LOAD_FLAGS_CACHE_MASK); + } + if (aLoadFlags & LOAD_FLAGS_VALIDATE_MASK) { + // Override the default validation flags... + requestFlags = (requestFlags & ~LOAD_FLAGS_VALIDATE_MASK) | + (aLoadFlags & LOAD_FLAGS_VALIDATE_MASK); + } + if (aLoadFlags & nsIRequest::LOAD_BACKGROUND) { + // Propagate background loading... + requestFlags |= nsIRequest::LOAD_BACKGROUND; + } + if (aLoadFlags & nsIRequest::LOAD_RECORD_START_REQUEST_DELAY) { + requestFlags |= nsIRequest::LOAD_RECORD_START_REQUEST_DELAY; + } + + if (aLinkPreload) { + // Set background loading if it is + requestFlags |= nsIRequest::LOAD_BACKGROUND; + } + + CORSMode corsmode = CORS_NONE; + if (aLoadFlags & imgILoader::LOAD_CORS_ANONYMOUS) { + corsmode = CORS_ANONYMOUS; + } else if (aLoadFlags & imgILoader::LOAD_CORS_USE_CREDENTIALS) { + corsmode = CORS_USE_CREDENTIALS; + } + + // Look in the preloaded images of loading document first. + if (StaticPrefs::network_preload() && !aLinkPreload && aLoadingDocument) { + // All Early Hints preloads are Link preloads, therefore we don't have a + // Early Hints preload here + MOZ_ASSERT(!aEarlyHintPreloaderId); + auto key = + PreloadHashKey::CreateAsImage(aURI, aTriggeringPrincipal, corsmode); + if (RefPtr preload = + aLoadingDocument->Preloads().LookupPreload(key)) { + RefPtr proxy = do_QueryObject(preload); + MOZ_ASSERT(proxy); + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::LoadImage -- preloaded [proxy=%p]" + " [document=%p]\n", + this, proxy.get(), aLoadingDocument)); + + // Removing the preload for this image to be in parity with Chromium. Any + // following regular image request will be reloaded using the regular + // path: image cache, http cache, network. Any following `` will start a new image preload that can be + // satisfied from http cache or network. + // + // There is a spec discussion for "preload cache", see + // https://github.com/w3c/preload/issues/97. And it is also not clear how + // preload image interacts with list of available images, see + // https://github.com/whatwg/html/issues/4474. + proxy->RemoveSelf(aLoadingDocument); + proxy->NotifyUsage(aLoadingDocument); + + imgRequest* request = proxy->GetOwner(); + nsresult rv = + CreateNewProxyForRequest(request, aURI, aLoadGroup, aLoadingDocument, + aObserver, requestFlags, _retval); + NS_ENSURE_SUCCESS(rv, rv); + + imgRequestProxy* newProxy = *_retval; + if (imgCacheValidator* validator = request->GetValidator()) { + newProxy->MarkValidating(); + // Attach the proxy without notifying and this will add us to the load + // group. + validator->AddProxy(newProxy); + } else { + // It's OK to add here even if the request is done. If it is, it'll send + // a OnStopRequest()and the proxy will be removed from the loadgroup in + // imgRequestProxy::OnLoadComplete. + newProxy->AddToLoadGroup(); + newProxy->NotifyListener(); + } + + return NS_OK; + } + } + + RefPtr entry; + + // Look in the cache for our URI, and then validate it. + // XXX For now ignore aCacheKey. We will need it in the future + // for correctly dealing with image load requests that are a result + // of post data. + OriginAttributes attrs; + if (aTriggeringPrincipal) { + attrs = aTriggeringPrincipal->OriginAttributesRef(); + } + ImageCacheKey key(aURI, corsmode, attrs, aLoadingDocument); + if (mCache.Get(key, getter_AddRefs(entry)) && entry) { + bool newChannelCreated = false; + if (ValidateEntry(entry, aURI, aInitialDocumentURI, aReferrerInfo, + aLoadGroup, aObserver, aLoadingDocument, requestFlags, + aContentPolicyType, true, &newChannelCreated, _retval, + aTriggeringPrincipal, corsmode, aLinkPreload, + aEarlyHintPreloaderId)) { + request = entry->GetRequest(); + + // If this entry has no proxies, its request has no reference to the + // entry. + if (entry->HasNoProxies()) { + LOG_FUNC_WITH_PARAM(gImgLog, + "imgLoader::LoadImage() adding proxyless entry", + "uri", key.URI()); + MOZ_ASSERT(!request->HasCacheEntry(), + "Proxyless entry's request has cache entry!"); + request->SetCacheEntry(entry); + + if (mCacheTracker && entry->GetExpirationState()->IsTracked()) { + mCacheTracker->MarkUsed(entry); + } + } + + entry->Touch(); + + if (!newChannelCreated) { + // This is ugly but it's needed to report CSP violations. We have 3 + // scenarios: + // - we don't have cache. We are not in this if() stmt. A new channel is + // created and that triggers the CSP checks. + // - We have a cache entry and this is blocked by CSP directives. + DebugOnly shouldLoad = ShouldLoadCachedImage( + request, aLoadingDocument, aTriggeringPrincipal, aContentPolicyType, + /* aSendCSPViolationReports */ true); + MOZ_ASSERT(shouldLoad); + } + } else { + // We can't use this entry. We'll try to load it off the network, and if + // successful, overwrite the old entry in the cache with a new one. + entry = nullptr; + } + } + + // Keep the channel in this scope, so we can adjust its notificationCallbacks + // later when we create the proxy. + nsCOMPtr newChannel; + // If we didn't get a cache hit, we need to load from the network. + if (!request) { + LOG_SCOPE(gImgLog, "imgLoader::LoadImage |cache miss|"); + + bool forcePrincipalCheck; + rv = NewImageChannel(getter_AddRefs(newChannel), &forcePrincipalCheck, aURI, + aInitialDocumentURI, corsmode, aReferrerInfo, + aLoadGroup, requestFlags, aContentPolicyType, + aTriggeringPrincipal, aContext, mRespectPrivacy, + aEarlyHintPreloaderId); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(NS_UsePrivateBrowsing(newChannel) == mRespectPrivacy); + + NewRequestAndEntry(forcePrincipalCheck, this, key, getter_AddRefs(request), + getter_AddRefs(entry)); + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::LoadImage -- Created new imgRequest" + " [request=%p]\n", + this, request.get())); + + nsCOMPtr cos(do_QueryInterface(newChannel)); + if (cos) { + if (aUseUrgentStartForChannel && !aLinkPreload) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + } + + if (StaticPrefs::network_http_tailing_enabled() && + aContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON) { + cos->AddClassFlags(nsIClassOfService::Throttleable | + nsIClassOfService::Tail); + nsCOMPtr httpChannel(do_QueryInterface(newChannel)); + if (httpChannel) { + Unused << httpChannel->SetRequestContextID(aRequestContextID); + } + } + } + + nsCOMPtr channelLoadGroup; + newChannel->GetLoadGroup(getter_AddRefs(channelLoadGroup)); + rv = request->Init(aURI, aURI, /* aHadInsecureRedirect = */ false, + channelLoadGroup, newChannel, entry, aLoadingDocument, + aTriggeringPrincipal, corsmode, aReferrerInfo); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // Add the initiator type for this image load + nsCOMPtr timedChannel = do_QueryInterface(newChannel); + if (timedChannel) { + timedChannel->SetInitiatorType(initiatorType); + } + + // create the proxy listener + nsCOMPtr listener = new ProxyListener(request.get()); + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::LoadImage -- Calling channel->AsyncOpen()\n", + this)); + + mozilla::net::PredictorLearn(aURI, aInitialDocumentURI, + nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE, + aLoadGroup); + + nsresult openRes; + openRes = newChannel->AsyncOpen(listener); + + if (NS_FAILED(openRes)) { + MOZ_LOG( + gImgLog, LogLevel::Debug, + ("[this=%p] imgLoader::LoadImage -- AsyncOpen() failed: 0x%" PRIx32 + "\n", + this, static_cast(openRes))); + request->CancelAndAbort(openRes); + return openRes; + } + + // Try to add the new request into the cache. + PutIntoCache(key, entry); + } else { + LOG_MSG_WITH_PARAM(gImgLog, "imgLoader::LoadImage |cache hit|", "request", + request); + } + + // If we didn't get a proxy when validating the cache entry, we need to + // create one. + if (!*_retval) { + // ValidateEntry() has three return values: "Is valid," "might be valid -- + // validating over network", and "not valid." If we don't have a _retval, + // we know ValidateEntry is not validating over the network, so it's safe + // to SetLoadId here because we know this request is valid for this context. + // + // Note, however, that this doesn't guarantee the behaviour we want (one + // URL maps to the same image on a page) if we load the same image in a + // different tab (see bug 528003), because its load id will get re-set, and + // that'll cause us to validate over the network. + request->SetLoadId(aLoadingDocument); + + LOG_MSG(gImgLog, "imgLoader::LoadImage", "creating proxy request."); + rv = CreateNewProxyForRequest(request, aURI, aLoadGroup, aLoadingDocument, + aObserver, requestFlags, _retval); + if (NS_FAILED(rv)) { + return rv; + } + + imgRequestProxy* proxy = *_retval; + + // Make sure that OnStatus/OnProgress calls have the right request set, if + // we did create a channel here. + if (newChannel) { + nsCOMPtr requestor( + new nsProgressNotificationProxy(newChannel, proxy)); + if (!requestor) { + return NS_ERROR_OUT_OF_MEMORY; + } + newChannel->SetNotificationCallbacks(requestor); + } + + if (aLinkPreload) { + MOZ_ASSERT(aLoadingDocument); + proxy->PrioritizeAsPreload(); + auto preloadKey = + PreloadHashKey::CreateAsImage(aURI, aTriggeringPrincipal, corsmode); + proxy->NotifyOpen(preloadKey, aLoadingDocument, true); + } + + // Note that it's OK to add here even if the request is done. If it is, + // it'll send a OnStopRequest() to the proxy in imgRequestProxy::Notify and + // the proxy will be removed from the loadgroup. + proxy->AddToLoadGroup(); + + // If we're loading off the network, explicitly don't notify our proxy, + // because necko (or things called from necko, such as imgCacheValidator) + // are going to call our notifications asynchronously, and we can't make it + // further asynchronous because observers might rely on imagelib completing + // its work between the channel's OnStartRequest and OnStopRequest. + if (!newChannel) { + proxy->NotifyListener(); + } + + return rv; + } + + NS_ASSERTION(*_retval, "imgLoader::LoadImage -- no return value"); + + return NS_OK; +} + +NS_IMETHODIMP +imgLoader::LoadImageWithChannelXPCOM(nsIChannel* channel, + imgINotificationObserver* aObserver, + Document* aLoadingDocument, + nsIStreamListener** listener, + imgIRequest** _retval) { + nsresult result; + imgRequestProxy* proxy; + result = LoadImageWithChannel(channel, aObserver, aLoadingDocument, listener, + &proxy); + *_retval = proxy; + return result; +} + +nsresult imgLoader::LoadImageWithChannel(nsIChannel* channel, + imgINotificationObserver* aObserver, + Document* aLoadingDocument, + nsIStreamListener** listener, + imgRequestProxy** _retval) { + NS_ASSERTION(channel, + "imgLoader::LoadImageWithChannel -- NULL channel pointer"); + + MOZ_ASSERT(NS_UsePrivateBrowsing(channel) == mRespectPrivacy); + + auto makeStaticIfNeeded = mozilla::MakeScopeExit( + [&] { MakeRequestStaticIfNeeded(aLoadingDocument, _retval); }); + + LOG_SCOPE(gImgLog, "imgLoader::LoadImageWithChannel"); + RefPtr request; + + nsCOMPtr uri; + channel->GetURI(getter_AddRefs(uri)); + + NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE); + nsCOMPtr loadInfo = channel->LoadInfo(); + + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + + // TODO: Get a meaningful cors mode from the caller probably? + const auto corsMode = CORS_NONE; + ImageCacheKey key(uri, corsMode, attrs, aLoadingDocument); + + nsLoadFlags requestFlags = nsIRequest::LOAD_NORMAL; + channel->GetLoadFlags(&requestFlags); + + RefPtr entry; + + if (requestFlags & nsIRequest::LOAD_BYPASS_CACHE) { + RemoveFromCache(key); + } else { + // Look in the cache for our URI, and then validate it. + // XXX For now ignore aCacheKey. We will need it in the future + // for correctly dealing with image load requests that are a result + // of post data. + if (mCache.Get(key, getter_AddRefs(entry)) && entry) { + // We don't want to kick off another network load. So we ask + // ValidateEntry to only do validation without creating a new proxy. If + // it says that the entry isn't valid any more, we'll only use the entry + // we're getting if the channel is loading from the cache anyways. + // + // XXX -- should this be changed? it's pretty much verbatim from the old + // code, but seems nonsensical. + // + // Since aCanMakeNewChannel == false, we don't need to pass content policy + // type/principal/etc + + nsCOMPtr loadInfo = channel->LoadInfo(); + // if there is a loadInfo, use the right contentType, otherwise + // default to the internal image type + nsContentPolicyType policyType = loadInfo->InternalContentPolicyType(); + + if (ValidateEntry(entry, uri, nullptr, nullptr, nullptr, aObserver, + aLoadingDocument, requestFlags, policyType, false, + nullptr, nullptr, nullptr, corsMode, false, 0)) { + request = entry->GetRequest(); + } else { + nsCOMPtr cacheChan(do_QueryInterface(channel)); + bool bUseCacheCopy; + + if (cacheChan) { + cacheChan->IsFromCache(&bUseCacheCopy); + } else { + bUseCacheCopy = false; + } + + if (!bUseCacheCopy) { + entry = nullptr; + } else { + request = entry->GetRequest(); + } + } + + if (request && entry) { + // If this entry has no proxies, its request has no reference to + // the entry. + if (entry->HasNoProxies()) { + LOG_FUNC_WITH_PARAM( + gImgLog, + "imgLoader::LoadImageWithChannel() adding proxyless entry", "uri", + key.URI()); + MOZ_ASSERT(!request->HasCacheEntry(), + "Proxyless entry's request has cache entry!"); + request->SetCacheEntry(entry); + + if (mCacheTracker && entry->GetExpirationState()->IsTracked()) { + mCacheTracker->MarkUsed(entry); + } + } + } + } + } + + nsCOMPtr loadGroup; + channel->GetLoadGroup(getter_AddRefs(loadGroup)); + +#ifdef DEBUG + if (aLoadingDocument) { + // The load group of the channel should always match that of the + // document if given. If that isn't the case, then we need to add more + // plumbing to ensure we block the document as well. + nsCOMPtr docLoadGroup = + aLoadingDocument->GetDocumentLoadGroup(); + MOZ_ASSERT(docLoadGroup == loadGroup); + } +#endif + + // Filter out any load flags not from nsIRequest + requestFlags &= nsIRequest::LOAD_REQUESTMASK; + + nsresult rv = NS_OK; + if (request) { + // we have this in our cache already.. cancel the current (document) load + + // this should fire an OnStopRequest + channel->Cancel(NS_ERROR_PARSED_DATA_CACHED); + + *listener = nullptr; // give them back a null nsIStreamListener + + rv = CreateNewProxyForRequest(request, uri, loadGroup, aLoadingDocument, + aObserver, requestFlags, _retval); + static_cast(*_retval)->NotifyListener(); + } else { + // We use originalURI here to fulfil the imgIRequest contract on GetURI. + nsCOMPtr originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + + // XXX(seth): We should be able to just use |key| here, except that |key| is + // constructed above with the *current URI* and not the *original URI*. I'm + // pretty sure this is a bug, and it's preventing us from ever getting a + // cache hit in LoadImageWithChannel when redirects are involved. + ImageCacheKey originalURIKey(originalURI, corsMode, attrs, + aLoadingDocument); + + // Default to doing a principal check because we don't know who + // started that load and whether their principal ended up being + // inherited on the channel. + NewRequestAndEntry(/* aForcePrincipalCheckForCacheEntry = */ true, this, + originalURIKey, getter_AddRefs(request), + getter_AddRefs(entry)); + + // No principal specified here, because we're not passed one. + // In LoadImageWithChannel, the redirects that may have been + // associated with this load would have gone through necko. + // We only have the final URI in ImageLib and hence don't know + // if the request went through insecure redirects. But if it did, + // the necko cache should have handled that (since all necko cache hits + // including the redirects will go through content policy). Hence, we + // can set aHadInsecureRedirect to false here. + rv = request->Init(originalURI, uri, /* aHadInsecureRedirect = */ false, + channel, channel, entry, aLoadingDocument, nullptr, + corsMode, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr pl = + new ProxyListener(static_cast(request.get())); + pl.forget(listener); + + // Try to add the new request into the cache. + PutIntoCache(originalURIKey, entry); + + rv = CreateNewProxyForRequest(request, originalURI, loadGroup, + aLoadingDocument, aObserver, requestFlags, + _retval); + + // Explicitly don't notify our proxy, because we're loading off the + // network, and necko (or things called from necko, such as + // imgCacheValidator) are going to call our notifications asynchronously, + // and we can't make it further asynchronous because observers might rely + // on imagelib completing its work between the channel's OnStartRequest and + // OnStopRequest. + } + + if (NS_FAILED(rv)) { + return rv; + } + + (*_retval)->AddToLoadGroup(); + return rv; +} + +bool imgLoader::SupportImageWithMimeType(const nsACString& aMimeType, + AcceptedMimeTypes aAccept + /* = AcceptedMimeTypes::IMAGES */) { + nsAutoCString mimeType(aMimeType); + ToLowerCase(mimeType); + + if (aAccept == AcceptedMimeTypes::IMAGES_AND_DOCUMENTS && + mimeType.EqualsLiteral("image/svg+xml")) { + return true; + } + + DecoderType type = DecoderFactory::GetDecoderType(mimeType.get()); + return type != DecoderType::UNKNOWN; +} + +NS_IMETHODIMP +imgLoader::GetMIMETypeFromContent(nsIRequest* aRequest, + const uint8_t* aContents, uint32_t aLength, + nsACString& aContentType) { + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + nsresult rv = + GetMimeTypeFromContent((const char*)aContents, aLength, aContentType); + if (NS_SUCCEEDED(rv) && channel && XRE_IsParentProcess()) { + if (RefPtr httpChannel = + do_QueryObject(channel)) { + // If the image type pattern matching algorithm given bytes does not + // return undefined, then disable the further check and allow the + // response. + httpChannel->DisableIsOpaqueResponseAllowedAfterSniffCheck( + mozilla::net::nsHttpChannel::SnifferType::Image); + } + } + + return rv; +} + +/* static */ +nsresult imgLoader::GetMimeTypeFromContent(const char* aContents, + uint32_t aLength, + nsACString& aContentType) { + nsAutoCString detected; + + /* Is it a GIF? */ + if (aLength >= 6 && + (!strncmp(aContents, "GIF87a", 6) || !strncmp(aContents, "GIF89a", 6))) { + aContentType.AssignLiteral(IMAGE_GIF); + + /* or a PNG? */ + } else if (aLength >= 8 && ((unsigned char)aContents[0] == 0x89 && + (unsigned char)aContents[1] == 0x50 && + (unsigned char)aContents[2] == 0x4E && + (unsigned char)aContents[3] == 0x47 && + (unsigned char)aContents[4] == 0x0D && + (unsigned char)aContents[5] == 0x0A && + (unsigned char)aContents[6] == 0x1A && + (unsigned char)aContents[7] == 0x0A)) { + aContentType.AssignLiteral(IMAGE_PNG); + + /* maybe a JPEG (JFIF)? */ + /* JFIF files start with SOI APP0 but older files can start with SOI DQT + * so we test for SOI followed by any marker, i.e. FF D8 FF + * this will also work for SPIFF JPEG files if they appear in the future. + * + * (JFIF is 0XFF 0XD8 0XFF 0XE0 0X4A 0X46 0X49 0X46 0X00) + */ + } else if (aLength >= 3 && ((unsigned char)aContents[0]) == 0xFF && + ((unsigned char)aContents[1]) == 0xD8 && + ((unsigned char)aContents[2]) == 0xFF) { + aContentType.AssignLiteral(IMAGE_JPEG); + + /* or how about ART? */ + /* ART begins with JG (4A 47). Major version offset 2. + * Minor version offset 3. Offset 4 must be nullptr. + */ + } else if (aLength >= 5 && ((unsigned char)aContents[0]) == 0x4a && + ((unsigned char)aContents[1]) == 0x47 && + ((unsigned char)aContents[4]) == 0x00) { + aContentType.AssignLiteral(IMAGE_ART); + + } else if (aLength >= 2 && !strncmp(aContents, "BM", 2)) { + aContentType.AssignLiteral(IMAGE_BMP); + + // ICOs always begin with a 2-byte 0 followed by a 2-byte 1. + // CURs begin with 2-byte 0 followed by 2-byte 2. + } else if (aLength >= 4 && (!memcmp(aContents, "\000\000\001\000", 4) || + !memcmp(aContents, "\000\000\002\000", 4))) { + aContentType.AssignLiteral(IMAGE_ICO); + + // WebPs always begin with RIFF, a 32-bit length, and WEBP. + } else if (aLength >= 12 && !memcmp(aContents, "RIFF", 4) && + !memcmp(aContents + 8, "WEBP", 4)) { + aContentType.AssignLiteral(IMAGE_WEBP); + + } else if (MatchesMP4(reinterpret_cast(aContents), aLength, + detected) && + detected.Equals(IMAGE_AVIF)) { + aContentType.AssignLiteral(IMAGE_AVIF); + } else if ((aLength >= 2 && !memcmp(aContents, "\xFF\x0A", 2)) || + (aLength >= 12 && + !memcmp(aContents, "\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A", 12))) { + // Each version is for containerless and containerful files respectively. + aContentType.AssignLiteral(IMAGE_JXL); + } else { + /* none of the above? I give up */ + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +/** + * proxy stream listener class used to handle multipart/x-mixed-replace + */ + +#include "nsIRequest.h" +#include "nsIStreamConverterService.h" + +NS_IMPL_ISUPPORTS(ProxyListener, nsIStreamListener, + nsIThreadRetargetableStreamListener, nsIRequestObserver) + +ProxyListener::ProxyListener(nsIStreamListener* dest) : mDestListener(dest) {} + +ProxyListener::~ProxyListener() = default; + +/** nsIRequestObserver methods **/ + +NS_IMETHODIMP +ProxyListener::OnStartRequest(nsIRequest* aRequest) { + if (!mDestListener) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (channel) { + // We need to set the initiator type for the image load + nsCOMPtr timedChannel = do_QueryInterface(channel); + if (timedChannel) { + nsAutoString type; + timedChannel->GetInitiatorType(type); + if (type.IsEmpty()) { + timedChannel->SetInitiatorType(u"img"_ns); + } + } + + nsAutoCString contentType; + nsresult rv = channel->GetContentType(contentType); + + if (!contentType.IsEmpty()) { + /* If multipart/x-mixed-replace content, we'll insert a MIME decoder + in the pipeline to handle the content and pass it along to our + original listener. + */ + if ("multipart/x-mixed-replace"_ns.Equals(contentType)) { + nsCOMPtr convServ( + do_GetService("@mozilla.org/streamConverters;1", &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr toListener(mDestListener); + nsCOMPtr fromListener; + + rv = convServ->AsyncConvertData("multipart/x-mixed-replace", "*/*", + toListener, nullptr, + getter_AddRefs(fromListener)); + if (NS_SUCCEEDED(rv)) { + mDestListener = fromListener; + } + } + } + } + } + + return mDestListener->OnStartRequest(aRequest); +} + +NS_IMETHODIMP +ProxyListener::OnStopRequest(nsIRequest* aRequest, nsresult status) { + if (!mDestListener) { + return NS_ERROR_FAILURE; + } + + return mDestListener->OnStopRequest(aRequest, status); +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +ProxyListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + if (!mDestListener) { + return NS_ERROR_FAILURE; + } + + return mDestListener->OnDataAvailable(aRequest, inStr, sourceOffset, count); +} + +/** nsThreadRetargetableStreamListener methods **/ +NS_IMETHODIMP +ProxyListener::CheckListenerChain() { + NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!"); + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mDestListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + MOZ_LOG( + gImgLog, LogLevel::Debug, + ("ProxyListener::CheckListenerChain %s [this=%p listener=%p rv=%" PRIx32 + "]", + (NS_SUCCEEDED(rv) ? "success" : "failure"), this, + (nsIStreamListener*)mDestListener, static_cast(rv))); + return rv; +} + +/** + * http validate class. check a channel for a 304 + */ + +NS_IMPL_ISUPPORTS(imgCacheValidator, nsIStreamListener, nsIRequestObserver, + nsIThreadRetargetableStreamListener, nsIChannelEventSink, + nsIInterfaceRequestor, nsIAsyncVerifyRedirectCallback) + +imgCacheValidator::imgCacheValidator(nsProgressNotificationProxy* progress, + imgLoader* loader, imgRequest* request, + Document* aDocument, + uint64_t aInnerWindowId, + bool forcePrincipalCheckForCacheEntry) + : mProgressProxy(progress), + mRequest(request), + mDocument(aDocument), + mInnerWindowId(aInnerWindowId), + mImgLoader(loader), + mHadInsecureRedirect(false) { + NewRequestAndEntry(forcePrincipalCheckForCacheEntry, loader, + mRequest->CacheKey(), getter_AddRefs(mNewRequest), + getter_AddRefs(mNewEntry)); +} + +imgCacheValidator::~imgCacheValidator() { + if (mRequest) { + // If something went wrong, and we never unblocked the requests waiting on + // validation, now is our last chance. We will cancel the new request and + // switch the waiting proxies to it. + UpdateProxies(/* aCancelRequest */ true, /* aSyncNotify */ false); + } +} + +void imgCacheValidator::AddProxy(imgRequestProxy* aProxy) { + // aProxy needs to be in the loadgroup since we're validating from + // the network. + aProxy->AddToLoadGroup(); + + mProxies.AppendElement(aProxy); +} + +void imgCacheValidator::RemoveProxy(imgRequestProxy* aProxy) { + mProxies.RemoveElement(aProxy); +} + +void imgCacheValidator::UpdateProxies(bool aCancelRequest, bool aSyncNotify) { + MOZ_ASSERT(mRequest); + + // Clear the validator before updating the proxies. The notifications may + // clone an existing request, and its state could be inconsistent. + mRequest->SetValidator(nullptr); + mRequest = nullptr; + + // If an error occurred, we will want to cancel the new request, and make the + // validating proxies point to it. Any proxies still bound to the original + // request which are not validating should remain untouched. + if (aCancelRequest) { + MOZ_ASSERT(mNewRequest); + mNewRequest->CancelAndAbort(NS_BINDING_ABORTED); + } + + // We have finished validating the request, so we can safely take ownership + // of the proxy list. imgRequestProxy::SyncNotifyListener can mutate the list + // if imgRequestProxy::CancelAndForgetObserver is called by its owner. Note + // that any potential notifications should still be suppressed in + // imgRequestProxy::ChangeOwner because we haven't cleared the validating + // flag yet, and thus they will remain deferred. + AutoTArray, 4> proxies(std::move(mProxies)); + + for (auto& proxy : proxies) { + // First update the state of all proxies before notifying any of them + // to ensure a consistent state (e.g. in case the notification causes + // other proxies to be touched indirectly.) + MOZ_ASSERT(proxy->IsValidating()); + MOZ_ASSERT(proxy->NotificationsDeferred(), + "Proxies waiting on cache validation should be " + "deferring notifications!"); + if (mNewRequest) { + proxy->ChangeOwner(mNewRequest); + } + proxy->ClearValidating(); + } + + mNewRequest = nullptr; + mNewEntry = nullptr; + + for (auto& proxy : proxies) { + if (aSyncNotify) { + // Notify synchronously, because the caller knows we are already in an + // asynchronously-called function (e.g. OnStartRequest). + proxy->SyncNotifyListener(); + } else { + // Notify asynchronously, because the caller does not know our current + // call state (e.g. ~imgCacheValidator). + proxy->NotifyListener(); + } + } +} + +/** nsIRequestObserver methods **/ + +NS_IMETHODIMP +imgCacheValidator::OnStartRequest(nsIRequest* aRequest) { + // We may be holding on to a document, so ensure that it's released. + RefPtr document = mDocument.forget(); + + // If for some reason we don't still have an existing request (probably + // because OnStartRequest got delivered more than once), just bail. + if (!mRequest) { + MOZ_ASSERT_UNREACHABLE("OnStartRequest delivered more than once?"); + aRequest->CancelWithReason(NS_BINDING_ABORTED, + "OnStartRequest delivered more than once?"_ns); + return NS_ERROR_FAILURE; + } + + // If this request is coming from cache and has the same URI as our + // imgRequest, the request all our proxies are pointing at is valid, and all + // we have to do is tell them to notify their listeners. + nsCOMPtr cacheChan(do_QueryInterface(aRequest)); + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (cacheChan && channel) { + bool isFromCache = false; + cacheChan->IsFromCache(&isFromCache); + + nsCOMPtr channelURI; + channel->GetURI(getter_AddRefs(channelURI)); + + nsCOMPtr finalURI; + mRequest->GetFinalURI(getter_AddRefs(finalURI)); + + bool sameURI = false; + if (channelURI && finalURI) { + channelURI->Equals(finalURI, &sameURI); + } + + if (isFromCache && sameURI) { + // We don't need to load this any more. + aRequest->CancelWithReason(NS_BINDING_ABORTED, + "imgCacheValidator::OnStartRequest"_ns); + mNewRequest = nullptr; + + // Clear the validator before updating the proxies. The notifications may + // clone an existing request, and its state could be inconsistent. + mRequest->SetLoadId(document); + mRequest->SetInnerWindowID(mInnerWindowId); + UpdateProxies(/* aCancelRequest */ false, /* aSyncNotify */ true); + return NS_OK; + } + } + + // We can't load out of cache. We have to create a whole new request for the + // data that's coming in off the channel. + nsCOMPtr uri; + mRequest->GetURI(getter_AddRefs(uri)); + + LOG_MSG_WITH_PARAM(gImgLog, + "imgCacheValidator::OnStartRequest creating new request", + "uri", uri); + + CORSMode corsmode = mRequest->GetCORSMode(); + nsCOMPtr referrerInfo = mRequest->GetReferrerInfo(); + nsCOMPtr triggeringPrincipal = + mRequest->GetTriggeringPrincipal(); + + // Doom the old request's cache entry + mRequest->RemoveFromCache(); + + // We use originalURI here to fulfil the imgIRequest contract on GetURI. + nsCOMPtr originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + nsresult rv = mNewRequest->Init(originalURI, uri, mHadInsecureRedirect, + aRequest, channel, mNewEntry, document, + triggeringPrincipal, corsmode, referrerInfo); + if (NS_FAILED(rv)) { + UpdateProxies(/* aCancelRequest */ true, /* aSyncNotify */ true); + return rv; + } + + mDestListener = new ProxyListener(mNewRequest); + + // Try to add the new request into the cache. Note that the entry must be in + // the cache before the proxies' ownership changes, because adding a proxy + // changes the caching behaviour for imgRequests. + mImgLoader->PutIntoCache(mNewRequest->CacheKey(), mNewEntry); + UpdateProxies(/* aCancelRequest */ false, /* aSyncNotify */ true); + return mDestListener->OnStartRequest(aRequest); +} + +NS_IMETHODIMP +imgCacheValidator::OnStopRequest(nsIRequest* aRequest, nsresult status) { + // Be sure we've released the document that we may have been holding on to. + mDocument = nullptr; + + if (!mDestListener) { + return NS_OK; + } + + return mDestListener->OnStopRequest(aRequest, status); +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +imgCacheValidator::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + if (!mDestListener) { + // XXX see bug 113959 + uint32_t _retval; + inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &_retval); + return NS_OK; + } + + return mDestListener->OnDataAvailable(aRequest, inStr, sourceOffset, count); +} + +/** nsIThreadRetargetableStreamListener methods **/ + +NS_IMETHODIMP +imgCacheValidator::CheckListenerChain() { + NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!"); + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mDestListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + MOZ_LOG( + gImgLog, LogLevel::Debug, + ("[this=%p] imgCacheValidator::CheckListenerChain -- rv %" PRId32 "=%s", + this, static_cast(rv), + NS_SUCCEEDED(rv) ? "succeeded" : "failed")); + return rv; +} + +/** nsIInterfaceRequestor methods **/ + +NS_IMETHODIMP +imgCacheValidator::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + return QueryInterface(aIID, aResult); + } + + return mProgressProxy->GetInterface(aIID, aResult); +} + +// These functions are materially the same as the same functions in imgRequest. +// We duplicate them because we're verifying whether cache loads are necessary, +// not unconditionally loading. + +/** nsIChannelEventSink methods **/ +NS_IMETHODIMP +imgCacheValidator::AsyncOnChannelRedirect( + nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + // Note all cache information we get from the old channel. + mNewRequest->SetCacheValidation(mNewEntry, oldChannel); + + // If the previous URI is a non-HTTPS URI, record that fact for later use by + // security code, which needs to know whether there is an insecure load at any + // point in the redirect chain. + nsCOMPtr oldURI; + bool schemeLocal = false; + if (NS_FAILED(oldChannel->GetURI(getter_AddRefs(oldURI))) || + NS_FAILED(NS_URIChainHasFlags( + oldURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &schemeLocal)) || + (!oldURI->SchemeIs("https") && !oldURI->SchemeIs("chrome") && + !schemeLocal)) { + mHadInsecureRedirect = true; + } + + // Prepare for callback + mRedirectCallback = callback; + mRedirectChannel = newChannel; + + return mProgressProxy->AsyncOnChannelRedirect(oldChannel, newChannel, flags, + this); +} + +NS_IMETHODIMP +imgCacheValidator::OnRedirectVerifyCallback(nsresult aResult) { + // If we've already been told to abort, just do so. + if (NS_FAILED(aResult)) { + mRedirectCallback->OnRedirectVerifyCallback(aResult); + mRedirectCallback = nullptr; + mRedirectChannel = nullptr; + return NS_OK; + } + + // make sure we have a protocol that returns data rather than opens + // an external application, e.g. mailto: + nsCOMPtr uri; + mRedirectChannel->GetURI(getter_AddRefs(uri)); + + nsresult result = NS_OK; + + if (nsContentUtils::IsExternalProtocol(uri)) { + result = NS_ERROR_ABORT; + } + + mRedirectCallback->OnRedirectVerifyCallback(result); + mRedirectCallback = nullptr; + mRedirectChannel = nullptr; + return NS_OK; +} diff --git a/image/imgLoader.h b/image/imgLoader.h new file mode 100644 index 0000000000..168b473333 --- /dev/null +++ b/image/imgLoader.h @@ -0,0 +1,534 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_imgLoader_h +#define mozilla_image_imgLoader_h + +#include "mozilla/Attributes.h" +#include "mozilla/CORSMode.h" +#include "mozilla/Mutex.h" +#include "mozilla/EnumSet.h" +#include "mozilla/UniquePtr.h" + +#include "imgILoader.h" +#include "imgICache.h" +#include "nsWeakReference.h" +#include "nsIContentSniffer.h" +#include "nsRefPtrHashtable.h" +#include "nsTHashSet.h" +#include "nsExpirationTracker.h" +#include "ImageCacheKey.h" +#include "imgRequest.h" +#include "nsIProgressEventSink.h" +#include "nsIChannel.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "imgIRequest.h" + +class imgLoader; +class imgRequestProxy; +class imgINotificationObserver; +class nsILoadGroup; +class imgCacheExpirationTracker; +class imgMemoryReporter; + +namespace mozilla { +namespace dom { +class Document; +} +} // namespace mozilla + +class imgCacheEntry { + public: + imgCacheEntry(imgLoader* loader, imgRequest* request, + bool aForcePrincipalCheck); + ~imgCacheEntry(); + + nsrefcnt AddRef() { + MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); + NS_ASSERT_OWNINGTHREAD(imgCacheEntry); + ++mRefCnt; + NS_LOG_ADDREF(this, mRefCnt, "imgCacheEntry", sizeof(*this)); + return mRefCnt; + } + + nsrefcnt Release() { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + NS_ASSERT_OWNINGTHREAD(imgCacheEntry); + --mRefCnt; + NS_LOG_RELEASE(this, mRefCnt, "imgCacheEntry"); + if (mRefCnt == 0) { + mRefCnt = 1; /* stabilize */ + delete this; + return 0; + } + return mRefCnt; + } + + uint32_t GetDataSize() const { return mDataSize; } + void SetDataSize(uint32_t aDataSize) { + int32_t oldsize = mDataSize; + mDataSize = aDataSize; + UpdateCache(mDataSize - oldsize); + } + + int32_t GetTouchedTime() const { return mTouchedTime; } + void SetTouchedTime(int32_t time) { + mTouchedTime = time; + Touch(/* updateTime = */ false); + } + + uint32_t GetLoadTime() const { return mLoadTime; } + + void UpdateLoadTime(); + + uint32_t GetExpiryTime() const { return mExpiryTime; } + void SetExpiryTime(uint32_t aExpiryTime) { + mExpiryTime = aExpiryTime; + Touch(); + } + + bool GetMustValidate() const { return mMustValidate; } + void SetMustValidate(bool aValidate) { + mMustValidate = aValidate; + Touch(); + } + + already_AddRefed GetRequest() const { + RefPtr req = mRequest; + return req.forget(); + } + + bool Evicted() const { return mEvicted; } + + nsExpirationState* GetExpirationState() { return &mExpirationState; } + + bool HasNoProxies() const { return mHasNoProxies; } + + bool ForcePrincipalCheck() const { return mForcePrincipalCheck; } + + bool HasNotified() const { return mHasNotified; } + void SetHasNotified() { + MOZ_ASSERT(!mHasNotified); + mHasNotified = true; + } + + imgLoader* Loader() const { return mLoader; } + + private: // methods + friend class imgLoader; + friend class imgCacheQueue; + void Touch(bool updateTime = true); + void UpdateCache(int32_t diff = 0); + void SetEvicted(bool evict) { mEvicted = evict; } + void SetHasNoProxies(bool hasNoProxies); + + // Private, unimplemented copy constructor. + imgCacheEntry(const imgCacheEntry&); + + private: // data + nsAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + imgLoader* mLoader; + RefPtr mRequest; + uint32_t mDataSize; + int32_t mTouchedTime; + uint32_t mLoadTime; + uint32_t mExpiryTime; + nsExpirationState mExpirationState; + bool mMustValidate : 1; + bool mEvicted : 1; + bool mHasNoProxies : 1; + bool mForcePrincipalCheck : 1; + bool mHasNotified : 1; +}; + +#include + +#define NS_IMGLOADER_CID \ + { /* c1354898-e3fe-4602-88a7-c4520c21cb4e */ \ + 0xc1354898, 0xe3fe, 0x4602, { \ + 0x88, 0xa7, 0xc4, 0x52, 0x0c, 0x21, 0xcb, 0x4e \ + } \ + } + +class imgCacheQueue { + public: + imgCacheQueue(); + void Remove(imgCacheEntry*); + void Push(imgCacheEntry*); + void MarkDirty(); + bool IsDirty(); + already_AddRefed Pop(); + void Refresh(); + uint32_t GetSize() const; + void UpdateSize(int32_t diff); + uint32_t GetNumElements() const; + bool Contains(imgCacheEntry* aEntry) const; + typedef nsTArray> queueContainer; + typedef queueContainer::iterator iterator; + typedef queueContainer::const_iterator const_iterator; + + iterator begin(); + const_iterator begin() const; + iterator end(); + const_iterator end() const; + + private: + queueContainer mQueue; + bool mDirty; + uint32_t mSize; +}; + +enum class AcceptedMimeTypes : uint8_t { + IMAGES, + IMAGES_AND_DOCUMENTS, +}; + +class imgLoader final : public imgILoader, + public nsIContentSniffer, + public imgICache, + public nsSupportsWeakReference, + public nsIObserver { + virtual ~imgLoader(); + + public: + using ImageCacheKey = mozilla::image::ImageCacheKey; + using imgCacheTable = + nsRefPtrHashtable, imgCacheEntry>; + using imgSet = nsTHashSet; + using Mutex = mozilla::Mutex; + + NS_DECL_ISUPPORTS + NS_DECL_IMGILOADER + NS_DECL_NSICONTENTSNIFFER + NS_DECL_IMGICACHE + NS_DECL_NSIOBSERVER + + /** + * Get the normal image loader instance that is used by gecko code, creating + * it if necessary. + */ + static imgLoader* NormalLoader(); + + /** + * Get the Private Browsing image loader instance that is used by gecko code, + * creating it if necessary. + */ + static imgLoader* PrivateBrowsingLoader(); + + /** + * Gecko code should use NormalLoader() or PrivateBrowsingLoader() to get the + * appropriate image loader. + * + * This constructor is public because the XPCOM module code that creates + * instances of "@mozilla.org/image/loader;1" / "@mozilla.org/image/cache;1" + * for nsIComponentManager.createInstance()/nsIServiceManager.getService() + * calls (now only made by add-ons) needs access to it. + * + * XXX We would like to get rid of the nsIServiceManager.getService (and + * nsIComponentManager.createInstance) method of creating imgLoader objects, + * but there are add-ons that are still using it. These add-ons don't + * actually do anything useful with the loaders that they create since nobody + * who creates an imgLoader using this method actually QIs to imgILoader and + * loads images. They all just QI to imgICache and either call clearCache() + * or findEntryProperties(). Since they're doing this on an imgLoader that + * has never loaded images, these calls are useless. It seems likely that + * the code that is doing this is just legacy code left over from a time when + * there was only one imgLoader instance for the entire process. (Nowadays + * the correct method to get an imgILoader/imgICache is to call + * imgITools::getImgCacheForDocument/imgITools::getImgLoaderForDocument.) + * All the same, even though what these add-ons are doing is a no-op, + * removing the nsIServiceManager.getService method of creating/getting an + * imgLoader objects would cause an exception in these add-ons that could + * break things. + */ + imgLoader(); + nsresult Init(); + + bool IsImageAvailable(nsIURI*, nsIPrincipal* aTriggeringPrincipal, + mozilla::CORSMode, mozilla::dom::Document*); + + [[nodiscard]] nsresult LoadImage( + nsIURI* aURI, nsIURI* aInitialDocumentURI, nsIReferrerInfo* aReferrerInfo, + nsIPrincipal* aLoadingPrincipal, uint64_t aRequestContextID, + nsILoadGroup* aLoadGroup, imgINotificationObserver* aObserver, + nsINode* aContext, mozilla::dom::Document* aLoadingDocument, + nsLoadFlags aLoadFlags, nsISupports* aCacheKey, + nsContentPolicyType aContentPolicyType, const nsAString& initiatorType, + bool aUseUrgentStartForChannel, bool aLinkPreload, + uint64_t aEarlyHintPreloaderId, imgRequestProxy** _retval); + + [[nodiscard]] nsresult LoadImageWithChannel( + nsIChannel* channel, imgINotificationObserver* aObserver, + mozilla::dom::Document* aLoadingDocument, nsIStreamListener** listener, + imgRequestProxy** _retval); + + static nsresult GetMimeTypeFromContent(const char* aContents, + uint32_t aLength, + nsACString& aContentType); + + /** + * Returns true if the given mime type may be interpreted as an image. + * + * Some MIME types may be interpreted as both images and documents. (At the + * moment only "image/svg+xml" falls into this category, but there may be more + * in the future.) Callers which want this function to return true for such + * MIME types should pass AcceptedMimeTypes::IMAGES_AND_DOCUMENTS for + * @aAccept. + * + * @param aMimeType The MIME type to evaluate. + * @param aAcceptedMimeTypes Which kinds of MIME types to treat as images. + */ + static bool SupportImageWithMimeType( + const nsACString&, AcceptedMimeTypes aAccept = AcceptedMimeTypes::IMAGES); + + static void GlobalInit(); // for use by the factory + static void Shutdown(); // for use by the factory + static void ShutdownMemoryReporter(); + + enum class ClearOption { + ChromeOnly, + UnusedOnly, + }; + using ClearOptions = mozilla::EnumSet; + nsresult ClearImageCache(ClearOptions = {}); + void MinimizeCache() { ClearImageCache({ClearOption::UnusedOnly}); } + + nsresult InitCache(); + + bool RemoveFromCache(const ImageCacheKey& aKey); + + // Enumeration describing if a given entry is in the cache queue or not. + // There are some cases we know the entry is definitely not in the queue. + enum class QueueState { MaybeExists, AlreadyRemoved }; + + bool RemoveFromCache(imgCacheEntry* entry, + QueueState aQueueState = QueueState::MaybeExists); + + bool PutIntoCache(const ImageCacheKey& aKey, imgCacheEntry* aEntry); + + void AddToUncachedImages(imgRequest* aRequest); + void RemoveFromUncachedImages(imgRequest* aRequest); + + // Returns true if we should prefer evicting cache entry |two| over cache + // entry |one|. + // This mixes units in the worst way, but provides reasonable results. + inline static bool CompareCacheEntries(const RefPtr& one, + const RefPtr& two) { + if (!one) { + return false; + } + if (!two) { + return true; + } + + const double sizeweight = 1.0 - sCacheTimeWeight; + + // We want large, old images to be evicted first (depending on their + // relative weights). Since a larger time is actually newer, we subtract + // time's weight, so an older image has a larger weight. + double oneweight = double(one->GetDataSize()) * sizeweight - + double(one->GetTouchedTime()) * sCacheTimeWeight; + double twoweight = double(two->GetDataSize()) * sizeweight - + double(two->GetTouchedTime()) * sCacheTimeWeight; + + return oneweight < twoweight; + } + + void VerifyCacheSizes(); + + nsresult RemoveEntriesInternal(nsIPrincipal* aPrincipal, + const nsACString* aBaseDomain); + + // The image loader maintains a hash table of all imgCacheEntries. However, + // only some of them will be evicted from the cache: those who have no + // imgRequestProxies watching their imgRequests. + // + // Once an imgRequest has no imgRequestProxies, it should notify us by + // calling HasNoObservers(), and null out its cache entry pointer. + // + // Upon having a proxy start observing again, it should notify us by calling + // HasObservers(). The request's cache entry will be re-set before this + // happens, by calling imgRequest::SetCacheEntry() when an entry with no + // observers is re-requested. + bool SetHasNoProxies(imgRequest* aRequest, imgCacheEntry* aEntry); + bool SetHasProxies(imgRequest* aRequest); + + private: // methods + static already_AddRefed CreateImageLoader(); + + bool ValidateEntry(imgCacheEntry* aEntry, nsIURI* aURI, + nsIURI* aInitialDocumentURI, + nsIReferrerInfo* aReferrerInfo, nsILoadGroup* aLoadGroup, + imgINotificationObserver* aObserver, + mozilla::dom::Document* aLoadingDocument, + nsLoadFlags aLoadFlags, + nsContentPolicyType aLoadPolicyType, + bool aCanMakeNewChannel, bool* aNewChannelCreated, + imgRequestProxy** aProxyRequest, + nsIPrincipal* aTriggeringPrincipal, mozilla::CORSMode, + bool aLinkPreload, uint64_t aEarlyHintPreloaderId); + + bool ValidateRequestWithNewChannel( + imgRequest* request, nsIURI* aURI, nsIURI* aInitialDocumentURI, + nsIReferrerInfo* aReferrerInfo, nsILoadGroup* aLoadGroup, + imgINotificationObserver* aObserver, + mozilla::dom::Document* aLoadingDocument, uint64_t aInnerWindowId, + nsLoadFlags aLoadFlags, nsContentPolicyType aContentPolicyType, + imgRequestProxy** aProxyRequest, nsIPrincipal* aLoadingPrincipal, + mozilla::CORSMode, bool aLinkPreload, uint64_t aEarlyHintPreloaderId, + bool* aNewChannelCreated); + + void NotifyObserversForCachedImage(imgCacheEntry* aEntry, imgRequest* request, + nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + mozilla::dom::Document* aLoadingDocument, + nsIPrincipal* aLoadingPrincipal, + mozilla::CORSMode, + uint64_t aEarlyHintPreloaderId); + // aURI may be different from imgRequest's URI in the case of blob URIs, as we + // can share requests with different URIs. + nsresult CreateNewProxyForRequest(imgRequest* aRequest, nsIURI* aURI, + nsILoadGroup* aLoadGroup, + mozilla::dom::Document* aLoadingDocument, + imgINotificationObserver* aObserver, + nsLoadFlags aLoadFlags, + imgRequestProxy** _retval); + + nsresult EvictEntries(bool aChromeOnly); + + void CacheEntriesChanged(int32_t aSizeDiff); + void CheckCacheLimits(); + + private: // data + friend class imgCacheEntry; + friend class imgMemoryReporter; + + imgCacheTable mCache; + imgCacheQueue mCacheQueue; + + // Hash set of every imgRequest for this loader that isn't in mCache or + // mChromeCache. The union over all imgLoader's of mCache, mChromeCache, and + // mUncachedImages should be every imgRequest that is alive. These are weak + // pointers so we rely on the imgRequest destructor to remove itself. + imgSet mUncachedImages MOZ_GUARDED_BY(mUncachedImagesMutex); + // The imgRequest can have refs to them held on non-main thread, so we need + // a mutex because we modify the uncached images set from the imgRequest + // destructor. + Mutex mUncachedImagesMutex; + + static double sCacheTimeWeight; + static uint32_t sCacheMaxSize; + static imgMemoryReporter* sMemReporter; + + mozilla::UniquePtr mCacheTracker; + bool mRespectPrivacy; +}; + +/** + * proxy stream listener class used to handle multipart/x-mixed-replace + */ + +#include "nsCOMPtr.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" + +class ProxyListener : public nsIStreamListener, + public nsIThreadRetargetableStreamListener { + public: + explicit ProxyListener(nsIStreamListener* dest); + + /* additional members */ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + private: + virtual ~ProxyListener(); + + nsCOMPtr mDestListener; +}; + +/** + * A class that implements nsIProgressEventSink and forwards all calls to it to + * the original notification callbacks of the channel. Also implements + * nsIInterfaceRequestor and gives out itself for nsIProgressEventSink calls, + * and forwards everything else to the channel's notification callbacks. + */ +class nsProgressNotificationProxy final : public nsIProgressEventSink, + public nsIChannelEventSink, + public nsIInterfaceRequestor { + public: + nsProgressNotificationProxy(nsIChannel* channel, imgIRequest* proxy) + : mImageRequest(proxy) { + channel->GetNotificationCallbacks(getter_AddRefs(mOriginalCallbacks)); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIPROGRESSEVENTSINK + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + private: + ~nsProgressNotificationProxy() = default; + + nsCOMPtr mOriginalCallbacks; + nsCOMPtr mImageRequest; +}; + +/** + * validate checker + */ + +#include "nsCOMArray.h" + +class imgCacheValidator : public nsIStreamListener, + public nsIThreadRetargetableStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsIAsyncVerifyRedirectCallback { + public: + imgCacheValidator(nsProgressNotificationProxy* progress, imgLoader* loader, + imgRequest* aRequest, mozilla::dom::Document* aDocument, + uint64_t aInnerWindowId, + bool forcePrincipalCheckForCacheEntry); + + void AddProxy(imgRequestProxy* aProxy); + void RemoveProxy(imgRequestProxy* aProxy); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK + + private: + void UpdateProxies(bool aCancelRequest, bool aSyncNotify); + virtual ~imgCacheValidator(); + + nsCOMPtr mDestListener; + RefPtr mProgressProxy; + nsCOMPtr mRedirectCallback; + nsCOMPtr mRedirectChannel; + + RefPtr mRequest; + AutoTArray, 4> mProxies; + + RefPtr mNewRequest; + RefPtr mNewEntry; + + RefPtr mDocument; + uint64_t mInnerWindowId; + + imgLoader* mImgLoader; + + bool mHadInsecureRedirect; +}; + +#endif // mozilla_image_imgLoader_h diff --git a/image/imgRequest.cpp b/image/imgRequest.cpp new file mode 100644 index 0000000000..9eaefa58d1 --- /dev/null +++ b/image/imgRequest.cpp @@ -0,0 +1,1231 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "imgRequest.h" +#include "ImageLogging.h" + +#include "imgLoader.h" +#include "imgRequestProxy.h" +#include "DecodePool.h" +#include "ProgressTracker.h" +#include "ImageFactory.h" +#include "Image.h" +#include "MultipartImage.h" +#include "RasterImage.h" + +#include "nsIChannel.h" +#include "nsICacheInfoChannel.h" +#include "nsIClassOfService.h" +#include "mozilla/dom/Document.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIInputStream.h" +#include "nsIMultiPartChannel.h" +#include "nsIHttpChannel.h" +#include "nsMimeTypes.h" + +#include "nsIInterfaceRequestorUtils.h" +#include "nsISupportsPrimitives.h" +#include "nsIScriptSecurityManager.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsEscape.h" + +#include "prtime.h" // for PR_Now +#include "nsNetUtil.h" +#include "nsIProtocolHandler.h" +#include "imgIRequest.h" +#include "nsProperties.h" +#include "nsIURL.h" + +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/SizeOfState.h" + +using namespace mozilla; +using namespace mozilla::image; + +#define LOG_TEST(level) (MOZ_LOG_TEST(gImgLog, (level))) + +NS_IMPL_ISUPPORTS(imgRequest, nsIStreamListener, nsIRequestObserver, + nsIThreadRetargetableStreamListener, nsIChannelEventSink, + nsIInterfaceRequestor, nsIAsyncVerifyRedirectCallback) + +imgRequest::imgRequest(imgLoader* aLoader, const ImageCacheKey& aCacheKey) + : mLoader(aLoader), + mCacheKey(aCacheKey), + mLoadId(nullptr), + mFirstProxy(nullptr), + mValidator(nullptr), + mCORSMode(CORS_NONE), + mImageErrorCode(NS_OK), + mImageAvailable(false), + mIsDeniedCrossSiteCORSRequest(false), + mIsCrossSiteNoCORSRequest(false), + mMutex("imgRequest"), + mProgressTracker(new ProgressTracker()), + mIsMultiPartChannel(false), + mIsInCache(false), + mDecodeRequested(false), + mNewPartPending(false), + mHadInsecureRedirect(false), + mInnerWindowId(0) { + LOG_FUNC(gImgLog, "imgRequest::imgRequest()"); +} + +imgRequest::~imgRequest() { + if (mLoader) { + mLoader->RemoveFromUncachedImages(this); + } + if (mURI) { + LOG_FUNC_WITH_PARAM(gImgLog, "imgRequest::~imgRequest()", "keyuri", mURI); + } else + LOG_FUNC(gImgLog, "imgRequest::~imgRequest()"); +} + +nsresult imgRequest::Init( + nsIURI* aURI, nsIURI* aFinalURI, bool aHadInsecureRedirect, + nsIRequest* aRequest, nsIChannel* aChannel, imgCacheEntry* aCacheEntry, + mozilla::dom::Document* aLoadingDocument, + nsIPrincipal* aTriggeringPrincipal, mozilla::CORSMode aCORSMode, + nsIReferrerInfo* aReferrerInfo) MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_ASSERT(NS_IsMainThread(), "Cannot use nsIURI off main thread!"); + // Init() can only be called once, and that's before it can be used off + // mainthread + + LOG_FUNC(gImgLog, "imgRequest::Init"); + + MOZ_ASSERT(!mImage, "Multiple calls to init"); + MOZ_ASSERT(aURI, "No uri"); + MOZ_ASSERT(aFinalURI, "No final uri"); + MOZ_ASSERT(aRequest, "No request"); + MOZ_ASSERT(aChannel, "No channel"); + + mProperties = new nsProperties(); + mURI = aURI; + mFinalURI = aFinalURI; + mRequest = aRequest; + mChannel = aChannel; + mTimedChannel = do_QueryInterface(mChannel); + mTriggeringPrincipal = aTriggeringPrincipal; + mCORSMode = aCORSMode; + mReferrerInfo = aReferrerInfo; + + // If the original URI and the final URI are different, check whether the + // original URI is secure. We deliberately don't take the final URI into + // account, as it needs to be handled using more complicated rules than + // earlier elements of the redirect chain. + if (aURI != aFinalURI) { + bool schemeLocal = false; + if (NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &schemeLocal)) || + (!aURI->SchemeIs("https") && !aURI->SchemeIs("chrome") && + !schemeLocal)) { + mHadInsecureRedirect = true; + } + } + + // imgCacheValidator may have handled redirects before we were created, so we + // allow the caller to let us know if any redirects were insecure. + mHadInsecureRedirect = mHadInsecureRedirect || aHadInsecureRedirect; + + mChannel->GetNotificationCallbacks(getter_AddRefs(mPrevChannelSink)); + + NS_ASSERTION(mPrevChannelSink != this, + "Initializing with a channel that already calls back to us!"); + + mChannel->SetNotificationCallbacks(this); + + mCacheEntry = aCacheEntry; + mCacheEntry->UpdateLoadTime(); + + SetLoadId(aLoadingDocument); + + // Grab the inner window ID of the loading document, if possible. + if (aLoadingDocument) { + mInnerWindowId = aLoadingDocument->InnerWindowID(); + } + + return NS_OK; +} + +bool imgRequest::CanReuseWithoutValidation(dom::Document* aDoc) const { + // If the request's loadId is the same as the aLoadingDocument, then it is ok + // to use this one because it has already been validated for this context. + // XXX: nullptr seems to be a 'special' key value that indicates that NO + // validation is required. + // XXX: we also check the window ID because the loadID() can return a reused + // pointer of a document. This can still happen for non-document image + // cache entries. + void* key = (void*)aDoc; + uint64_t innerWindowID = aDoc ? aDoc->InnerWindowID() : 0; + if (LoadId() == key && InnerWindowID() == innerWindowID) { + return true; + } + + // As a special-case, if this is a print preview document, also validate on + // the original document. This allows to print uncacheable images. + if (dom::Document* original = aDoc ? aDoc->GetOriginalDocument() : nullptr) { + return CanReuseWithoutValidation(original); + } + + return false; +} + +void imgRequest::ClearLoader() { mLoader = nullptr; } + +already_AddRefed imgRequest::GetTriggeringPrincipal() const { + nsCOMPtr principal = mTriggeringPrincipal; + return principal.forget(); +} + +already_AddRefed imgRequest::GetProgressTracker() const { + MutexAutoLock lock(mMutex); + + if (mImage) { + MOZ_ASSERT(!mProgressTracker, + "Should have given mProgressTracker to mImage"); + return mImage->GetProgressTracker(); + } + MOZ_ASSERT(mProgressTracker, + "Should have mProgressTracker until we create mImage"); + RefPtr progressTracker = mProgressTracker; + MOZ_ASSERT(progressTracker); + return progressTracker.forget(); +} + +void imgRequest::SetCacheEntry(imgCacheEntry* entry) { mCacheEntry = entry; } + +bool imgRequest::HasCacheEntry() const { return mCacheEntry != nullptr; } + +void imgRequest::ResetCacheEntry() { + if (HasCacheEntry()) { + mCacheEntry->SetDataSize(0); + } +} + +void imgRequest::AddProxy(imgRequestProxy* proxy) { + MOZ_ASSERT(proxy, "null imgRequestProxy passed in"); + LOG_SCOPE_WITH_PARAM(gImgLog, "imgRequest::AddProxy", "proxy", proxy); + + if (!mFirstProxy) { + // Save a raw pointer to the first proxy we see, for use in the network + // priority logic. + mFirstProxy = proxy; + } + + // If we're empty before adding, we have to tell the loader we now have + // proxies. + RefPtr progressTracker = GetProgressTracker(); + if (progressTracker->ObserverCount() == 0) { + MOZ_ASSERT(mURI, "Trying to SetHasProxies without key uri."); + if (mLoader) { + mLoader->SetHasProxies(this); + } + } + + progressTracker->AddObserver(proxy); +} + +nsresult imgRequest::RemoveProxy(imgRequestProxy* proxy, nsresult aStatus) { + LOG_SCOPE_WITH_PARAM(gImgLog, "imgRequest::RemoveProxy", "proxy", proxy); + + // This will remove our animation consumers, so after removing + // this proxy, we don't end up without proxies with observers, but still + // have animation consumers. + proxy->ClearAnimationConsumers(); + + // Let the status tracker do its thing before we potentially call Cancel() + // below, because Cancel() may result in OnStopRequest being called back + // before Cancel() returns, leaving the image in a different state then the + // one it was in at this point. + RefPtr progressTracker = GetProgressTracker(); + if (!progressTracker->RemoveObserver(proxy)) { + return NS_OK; + } + + if (progressTracker->ObserverCount() == 0) { + // If we have no observers, there's nothing holding us alive. If we haven't + // been cancelled and thus removed from the cache, tell the image loader so + // we can be evicted from the cache. + if (mCacheEntry) { + MOZ_ASSERT(mURI, "Removing last observer without key uri."); + + if (mLoader) { + mLoader->SetHasNoProxies(this, mCacheEntry); + } + } else { + LOG_MSG_WITH_PARAM(gImgLog, "imgRequest::RemoveProxy no cache entry", + "uri", mURI); + } + + /* If |aStatus| is a failure code, then cancel the load if it is still in + progress. Otherwise, let the load continue, keeping 'this' in the cache + with no observers. This way, if a proxy is destroyed without calling + cancel on it, it won't leak and won't leave a bad pointer in the observer + list. + */ + if (!(progressTracker->GetProgress() & FLAG_LAST_PART_COMPLETE) && + NS_FAILED(aStatus)) { + LOG_MSG(gImgLog, "imgRequest::RemoveProxy", + "load in progress. canceling"); + + this->Cancel(NS_BINDING_ABORTED); + } + + /* break the cycle from the cache entry. */ + mCacheEntry = nullptr; + } + + return NS_OK; +} + +uint64_t imgRequest::InnerWindowID() const { + MutexAutoLock lock(mMutex); + return mInnerWindowId; +} + +void imgRequest::SetInnerWindowID(uint64_t aInnerWindowId) { + MutexAutoLock lock(mMutex); + mInnerWindowId = aInnerWindowId; +} + +void imgRequest::CancelAndAbort(nsresult aStatus) { + LOG_SCOPE(gImgLog, "imgRequest::CancelAndAbort"); + + Cancel(aStatus); + + // It's possible for the channel to fail to open after we've set our + // notification callbacks. In that case, make sure to break the cycle between + // the channel and us, because it won't. + if (mChannel) { + mChannel->SetNotificationCallbacks(mPrevChannelSink); + mPrevChannelSink = nullptr; + } +} + +class imgRequestMainThreadCancel : public Runnable { + public: + imgRequestMainThreadCancel(imgRequest* aImgRequest, nsresult aStatus) + : Runnable("imgRequestMainThreadCancel"), + mImgRequest(aImgRequest), + mStatus(aStatus) { + MOZ_ASSERT(!NS_IsMainThread(), "Create me off main thread only!"); + MOZ_ASSERT(aImgRequest); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "I should be running on the main thread!"); + mImgRequest->ContinueCancel(mStatus); + return NS_OK; + } + + private: + RefPtr mImgRequest; + nsresult mStatus; +}; + +void imgRequest::Cancel(nsresult aStatus) { + /* The Cancel() method here should only be called by this class. */ + LOG_SCOPE(gImgLog, "imgRequest::Cancel"); + + if (NS_IsMainThread()) { + ContinueCancel(aStatus); + } else { + RefPtr progressTracker = GetProgressTracker(); + nsCOMPtr eventTarget = progressTracker->GetEventTarget(); + nsCOMPtr ev = new imgRequestMainThreadCancel(this, aStatus); + eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL); + } +} + +void imgRequest::ContinueCancel(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr progressTracker = GetProgressTracker(); + progressTracker->SyncNotifyProgress(FLAG_HAS_ERROR); + + RemoveFromCache(); + + if (mRequest && !(progressTracker->GetProgress() & FLAG_LAST_PART_COMPLETE)) { + mRequest->CancelWithReason(aStatus, "imgRequest::ContinueCancel"_ns); + } +} + +class imgRequestMainThreadEvict : public Runnable { + public: + explicit imgRequestMainThreadEvict(imgRequest* aImgRequest) + : Runnable("imgRequestMainThreadEvict"), mImgRequest(aImgRequest) { + MOZ_ASSERT(!NS_IsMainThread(), "Create me off main thread only!"); + MOZ_ASSERT(aImgRequest); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "I should be running on the main thread!"); + mImgRequest->ContinueEvict(); + return NS_OK; + } + + private: + RefPtr mImgRequest; +}; + +// EvictFromCache() is written to allowed to get called from any thread +void imgRequest::EvictFromCache() { + /* The EvictFromCache() method here should only be called by this class. */ + LOG_SCOPE(gImgLog, "imgRequest::EvictFromCache"); + + if (NS_IsMainThread()) { + ContinueEvict(); + } else { + NS_DispatchToMainThread(new imgRequestMainThreadEvict(this)); + } +} + +// Helper-method used by EvictFromCache() +void imgRequest::ContinueEvict() { + MOZ_ASSERT(NS_IsMainThread()); + + RemoveFromCache(); +} + +void imgRequest::StartDecoding() { + MutexAutoLock lock(mMutex); + mDecodeRequested = true; +} + +bool imgRequest::IsDecodeRequested() const { + MutexAutoLock lock(mMutex); + return mDecodeRequested; +} + +nsresult imgRequest::GetURI(nsIURI** aURI) { + MOZ_ASSERT(aURI); + + LOG_FUNC(gImgLog, "imgRequest::GetURI"); + + if (mURI) { + *aURI = mURI; + NS_ADDREF(*aURI); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +nsresult imgRequest::GetFinalURI(nsIURI** aURI) { + MOZ_ASSERT(aURI); + + LOG_FUNC(gImgLog, "imgRequest::GetFinalURI"); + + if (mFinalURI) { + *aURI = mFinalURI; + NS_ADDREF(*aURI); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +bool imgRequest::IsChrome() const { return mURI->SchemeIs("chrome"); } + +bool imgRequest::IsData() const { return mURI->SchemeIs("data"); } + +nsresult imgRequest::GetImageErrorCode() { return mImageErrorCode; } + +void imgRequest::RemoveFromCache() { + LOG_SCOPE(gImgLog, "imgRequest::RemoveFromCache"); + + bool isInCache = false; + + { + MutexAutoLock lock(mMutex); + isInCache = mIsInCache; + } + + if (isInCache && mLoader) { + // mCacheEntry is nulled out when we have no more observers. + if (mCacheEntry) { + mLoader->RemoveFromCache(mCacheEntry); + } else { + mLoader->RemoveFromCache(mCacheKey); + } + } + + mCacheEntry = nullptr; +} + +bool imgRequest::HasConsumers() const { + RefPtr progressTracker = GetProgressTracker(); + return progressTracker && progressTracker->ObserverCount() > 0; +} + +already_AddRefed imgRequest::GetImage() const { + MutexAutoLock lock(mMutex); + RefPtr image = mImage; + return image.forget(); +} + +void imgRequest::GetFileName(nsACString& aFileName) { + nsAutoString fileName; + + nsCOMPtr supportscstr; + if (NS_SUCCEEDED(mProperties->Get("content-disposition", + NS_GET_IID(nsISupportsCString), + getter_AddRefs(supportscstr))) && + supportscstr) { + nsAutoCString cdHeader; + supportscstr->GetData(cdHeader); + NS_GetFilenameFromDisposition(fileName, cdHeader); + } + + if (fileName.IsEmpty()) { + nsCOMPtr imgUrl(do_QueryInterface(mURI)); + if (imgUrl) { + imgUrl->GetFileName(aFileName); + NS_UnescapeURL(aFileName); + } + } else { + aFileName = NS_ConvertUTF16toUTF8(fileName); + } +} + +int32_t imgRequest::Priority() const { + int32_t priority = nsISupportsPriority::PRIORITY_NORMAL; + nsCOMPtr p = do_QueryInterface(mRequest); + if (p) { + p->GetPriority(&priority); + } + return priority; +} + +void imgRequest::AdjustPriority(imgRequestProxy* proxy, int32_t delta) { + // only the first proxy is allowed to modify the priority of this image load. + // + // XXX(darin): this is probably not the most optimal algorithm as we may want + // to increase the priority of requests that have a lot of proxies. the key + // concern though is that image loads remain lower priority than other pieces + // of content such as link clicks, CSS, and JS. + // + if (!mFirstProxy || proxy != mFirstProxy) { + return; + } + + AdjustPriorityInternal(delta); +} + +void imgRequest::AdjustPriorityInternal(int32_t aDelta) { + nsCOMPtr p = do_QueryInterface(mChannel); + if (p) { + p->AdjustPriority(aDelta); + } +} + +void imgRequest::BoostPriority(uint32_t aCategory) { + if (!StaticPrefs::image_layout_network_priority()) { + return; + } + + uint32_t newRequestedCategory = + (mBoostCategoriesRequested & aCategory) ^ aCategory; + if (!newRequestedCategory) { + // priority boost for each category can only apply once. + return; + } + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("[this=%p] imgRequest::BoostPriority for category %x", this, + newRequestedCategory)); + + int32_t delta = 0; + + if (newRequestedCategory & imgIRequest::CATEGORY_FRAME_INIT) { + --delta; + } + + if (newRequestedCategory & imgIRequest::CATEGORY_FRAME_STYLE) { + --delta; + } + + if (newRequestedCategory & imgIRequest::CATEGORY_SIZE_QUERY) { + --delta; + } + + if (newRequestedCategory & imgIRequest::CATEGORY_DISPLAY) { + delta += nsISupportsPriority::PRIORITY_HIGH; + } + + AdjustPriorityInternal(delta); + mBoostCategoriesRequested |= newRequestedCategory; +} + +void imgRequest::SetIsInCache(bool aInCache) { + LOG_FUNC_WITH_PARAM(gImgLog, "imgRequest::SetIsCacheable", "aInCache", + aInCache); + MutexAutoLock lock(mMutex); + mIsInCache = aInCache; +} + +void imgRequest::UpdateCacheEntrySize() { + if (!mCacheEntry) { + return; + } + + RefPtr image = GetImage(); + SizeOfState state(moz_malloc_size_of); + size_t size = image->SizeOfSourceWithComputedFallback(state); + mCacheEntry->SetDataSize(size); +} + +void imgRequest::SetCacheValidation(imgCacheEntry* aCacheEntry, + nsIRequest* aRequest) { + /* get the expires info */ + if (!aCacheEntry || aCacheEntry->GetExpiryTime() != 0) { + return; + } + + RefPtr req = aCacheEntry->GetRequest(); + MOZ_ASSERT(req); + RefPtr uri; + req->GetURI(getter_AddRefs(uri)); + // TODO(emilio): Seems we should be able to assert `uri` is not null, but we + // get here in such cases sometimes (like for some redirects, see + // docshell/test/chrome/test_bug89419.xhtml). + // + // We have the original URI in the cache key though, probably we should be + // using that instead of relying on Init() getting called. + auto info = nsContentUtils::GetSubresourceCacheValidationInfo(aRequest, uri); + + // Expiration time defaults to 0. We set the expiration time on our entry if + // it hasn't been set yet. + if (!info.mExpirationTime) { + // If the channel doesn't support caching, then ensure this expires the + // next time it is used. + info.mExpirationTime.emplace(nsContentUtils::SecondsFromPRTime(PR_Now()) - + 1); + } + aCacheEntry->SetExpiryTime(*info.mExpirationTime); + // Cache entries default to not needing to validate. We ensure that + // multiple calls to this function don't override an earlier decision to + // validate by making validation a one-way decision. + if (info.mMustRevalidate) { + aCacheEntry->SetMustValidate(info.mMustRevalidate); + } +} + +bool imgRequest::GetMultipart() const { + MutexAutoLock lock(mMutex); + return mIsMultiPartChannel; +} + +bool imgRequest::HadInsecureRedirect() const { + MutexAutoLock lock(mMutex); + return mHadInsecureRedirect; +} + +/** nsIRequestObserver methods **/ + +NS_IMETHODIMP +imgRequest::OnStartRequest(nsIRequest* aRequest) { + LOG_SCOPE(gImgLog, "imgRequest::OnStartRequest"); + + RefPtr image; + + if (nsCOMPtr httpChannel = do_QueryInterface(aRequest)) { + nsresult rv; + nsCOMPtr loadInfo = httpChannel->LoadInfo(); + mIsDeniedCrossSiteCORSRequest = + loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)); + mIsCrossSiteNoCORSRequest = loadInfo->GetTainting() == LoadTainting::Opaque; + } + + // Figure out if we're multipart. + nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); + { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(multiPartChannel || !mIsMultiPartChannel, + "Stopped being multipart?"); + + mNewPartPending = true; + image = mImage; + mIsMultiPartChannel = bool(multiPartChannel); + } + + // If we're not multipart, we shouldn't have an image yet. + if (image && !multiPartChannel) { + MOZ_ASSERT_UNREACHABLE("Already have an image for a non-multipart request"); + Cancel(NS_IMAGELIB_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + /* + * If mRequest is null here, then we need to set it so that we'll be able to + * cancel it if our Cancel() method is called. Note that this can only + * happen for multipart channels. We could simply not null out mRequest for + * non-last parts, if GetIsLastPart() were reliable, but it's not. See + * https://bugzilla.mozilla.org/show_bug.cgi?id=339610 + */ + if (!mRequest) { + MOZ_ASSERT(multiPartChannel, "Should have mRequest unless we're multipart"); + nsCOMPtr baseChannel; + multiPartChannel->GetBaseChannel(getter_AddRefs(baseChannel)); + mRequest = baseChannel; + } + + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (channel) { + /* Get our principal */ + nsCOMPtr secMan = + nsContentUtils::GetSecurityManager(); + if (secMan) { + nsresult rv = secMan->GetChannelResultPrincipal( + channel, getter_AddRefs(mPrincipal)); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + SetCacheValidation(mCacheEntry, aRequest); + + // Shouldn't we be dead already if this gets hit? + // Probably multipart/x-mixed-replace... + RefPtr progressTracker = GetProgressTracker(); + if (progressTracker->ObserverCount() == 0) { + this->Cancel(NS_IMAGELIB_ERROR_FAILURE); + } + + // Try to retarget OnDataAvailable to a decode thread. We must process data + // URIs synchronously as per the spec however. + if (!channel || IsData()) { + return NS_OK; + } + + nsCOMPtr retargetable = + do_QueryInterface(aRequest); + if (retargetable) { + nsAutoCString mimeType; + nsresult rv = channel->GetContentType(mimeType); + if (NS_SUCCEEDED(rv) && !mimeType.EqualsLiteral(IMAGE_SVG_XML)) { + // Retarget OnDataAvailable to the DecodePool's IO thread. + nsCOMPtr target = + DecodePool::Singleton()->GetIOEventTarget(); + rv = retargetable->RetargetDeliveryTo(target); + } + MOZ_LOG(gImgLog, LogLevel::Warning, + ("[this=%p] imgRequest::OnStartRequest -- " + "RetargetDeliveryTo rv %" PRIu32 "=%s\n", + this, static_cast(rv), + NS_SUCCEEDED(rv) ? "succeeded" : "failed")); + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequest::OnStopRequest(nsIRequest* aRequest, nsresult status) { + LOG_FUNC(gImgLog, "imgRequest::OnStopRequest"); + MOZ_ASSERT(NS_IsMainThread(), "Can't send notifications off-main-thread"); + + RefPtr image = GetImage(); + + RefPtr strongThis = this; + + bool isMultipart = false; + bool newPartPending = false; + { + MutexAutoLock lock(mMutex); + isMultipart = mIsMultiPartChannel; + newPartPending = mNewPartPending; + } + if (isMultipart && newPartPending) { + OnDataAvailable(aRequest, nullptr, 0, 0); + } + + // XXXldb What if this is a non-last part of a multipart request? + // xxx before we release our reference to mRequest, lets + // save the last status that we saw so that the + // imgRequestProxy will have access to it. + if (mRequest) { + mRequest = nullptr; // we no longer need the request + } + + // stop holding a ref to the channel, since we don't need it anymore + if (mChannel) { + mChannel->SetNotificationCallbacks(mPrevChannelSink); + mPrevChannelSink = nullptr; + mChannel = nullptr; + } + + bool lastPart = true; + nsCOMPtr mpchan(do_QueryInterface(aRequest)); + if (mpchan) { + mpchan->GetIsLastPart(&lastPart); + } + + bool isPartial = false; + if (image && (status == NS_ERROR_NET_PARTIAL_TRANSFER)) { + isPartial = true; + status = NS_OK; // fake happy face + } + + // Tell the image that it has all of the source data. Note that this can + // trigger a failure, since the image might be waiting for more non-optional + // data and this is the point where we break the news that it's not coming. + if (image) { + nsresult rv = image->OnImageDataComplete(aRequest, status, lastPart); + + // If we got an error in the OnImageDataComplete() call, we don't want to + // proceed as if nothing bad happened. However, we also want to give + // precedence to failure status codes from necko, since presumably they're + // more meaningful. + if (NS_FAILED(rv) && NS_SUCCEEDED(status)) { + status = rv; + } + } + + // If the request went through, update the cache entry size. Otherwise, + // cancel the request, which removes us from the cache. + if (image && NS_SUCCEEDED(status) && !isPartial) { + // We update the cache entry size here because this is where we finish + // loading compressed source data, which is part of our size calculus. + UpdateCacheEntrySize(); + + } else if (isPartial) { + // Remove the partial image from the cache. + this->EvictFromCache(); + + } else { + mImageErrorCode = status; + + // if the error isn't "just" a partial transfer + // stops animations, removes from cache + this->Cancel(status); + } + + if (!image) { + // We have to fire the OnStopRequest notifications ourselves because there's + // no image capable of doing so. + Progress progress = + LoadCompleteProgress(lastPart, /* aError = */ false, status); + + RefPtr progressTracker = GetProgressTracker(); + progressTracker->SyncNotifyProgress(progress); + } + + mTimedChannel = nullptr; + return NS_OK; +} + +struct mimetype_closure { + nsACString* newType; +}; + +/* prototype for these defined below */ +static nsresult sniff_mimetype_callback(nsIInputStream* in, void* closure, + const char* fromRawSegment, + uint32_t toOffset, uint32_t count, + uint32_t* writeCount); + +/** nsThreadRetargetableStreamListener methods **/ +NS_IMETHODIMP +imgRequest::CheckListenerChain() { + // TODO Might need more checking here. + NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!"); + return NS_OK; +} + +/** nsIStreamListener methods **/ + +struct NewPartResult final { + explicit NewPartResult(image::Image* aExistingImage) + : mImage(aExistingImage), + mIsFirstPart(!aExistingImage), + mSucceeded(false), + mShouldResetCacheEntry(false) {} + + nsAutoCString mContentType; + nsAutoCString mContentDisposition; + RefPtr mImage; + const bool mIsFirstPart; + bool mSucceeded; + bool mShouldResetCacheEntry; +}; + +static NewPartResult PrepareForNewPart(nsIRequest* aRequest, + nsIInputStream* aInStr, uint32_t aCount, + nsIURI* aURI, bool aIsMultipart, + image::Image* aExistingImage, + ProgressTracker* aProgressTracker, + uint64_t aInnerWindowId) { + NewPartResult result(aExistingImage); + + if (aInStr) { + mimetype_closure closure; + closure.newType = &result.mContentType; + + // Look at the first few bytes and see if we can tell what the data is from + // that since servers tend to lie. :( + uint32_t out; + aInStr->ReadSegments(sniff_mimetype_callback, &closure, aCount, &out); + } + + nsCOMPtr chan(do_QueryInterface(aRequest)); + if (result.mContentType.IsEmpty()) { + nsresult rv = + chan ? chan->GetContentType(result.mContentType) : NS_ERROR_FAILURE; + if (NS_FAILED(rv)) { + MOZ_LOG(gImgLog, LogLevel::Error, + ("imgRequest::PrepareForNewPart -- " + "Content type unavailable from the channel\n")); + if (!aIsMultipart) { + return result; + } + } + } + + if (chan) { + chan->GetContentDispositionHeader(result.mContentDisposition); + } + + MOZ_LOG(gImgLog, LogLevel::Debug, + ("imgRequest::PrepareForNewPart -- Got content type %s\n", + result.mContentType.get())); + + // XXX If server lied about mimetype and it's SVG, we may need to copy + // the data and dispatch back to the main thread, AND tell the channel to + // dispatch there in the future. + + // Create the new image and give it ownership of our ProgressTracker. + if (aIsMultipart) { + // Create the ProgressTracker and image for this part. + RefPtr progressTracker = new ProgressTracker(); + RefPtr partImage = image::ImageFactory::CreateImage( + aRequest, progressTracker, result.mContentType, aURI, + /* aIsMultipart = */ true, aInnerWindowId); + + if (result.mIsFirstPart) { + // First part for a multipart channel. Create the MultipartImage wrapper. + MOZ_ASSERT(aProgressTracker, "Shouldn't have given away tracker yet"); + aProgressTracker->SetIsMultipart(); + result.mImage = image::ImageFactory::CreateMultipartImage( + partImage, aProgressTracker); + } else { + // Transition to the new part. + auto multipartImage = static_cast(aExistingImage); + multipartImage->BeginTransitionToPart(partImage); + + // Reset our cache entry size so it doesn't keep growing without bound. + result.mShouldResetCacheEntry = true; + } + } else { + MOZ_ASSERT(!aExistingImage, "New part for non-multipart channel?"); + MOZ_ASSERT(aProgressTracker, "Shouldn't have given away tracker yet"); + + // Create an image using our progress tracker. + result.mImage = image::ImageFactory::CreateImage( + aRequest, aProgressTracker, result.mContentType, aURI, + /* aIsMultipart = */ false, aInnerWindowId); + } + + MOZ_ASSERT(result.mImage); + if (!result.mImage->HasError() || aIsMultipart) { + // We allow multipart images to fail to initialize (which generally + // indicates a bad content type) without cancelling the load, because + // subsequent parts might be fine. + result.mSucceeded = true; + } + + return result; +} + +class FinishPreparingForNewPartRunnable final : public Runnable { + public: + FinishPreparingForNewPartRunnable(imgRequest* aImgRequest, + NewPartResult&& aResult) + : Runnable("FinishPreparingForNewPartRunnable"), + mImgRequest(aImgRequest), + mResult(aResult) { + MOZ_ASSERT(aImgRequest); + } + + NS_IMETHOD Run() override { + mImgRequest->FinishPreparingForNewPart(mResult); + return NS_OK; + } + + private: + RefPtr mImgRequest; + NewPartResult mResult; +}; + +void imgRequest::FinishPreparingForNewPart(const NewPartResult& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + mContentType = aResult.mContentType; + + SetProperties(aResult.mContentType, aResult.mContentDisposition); + + if (aResult.mIsFirstPart) { + // Notify listeners that we have an image. + mImageAvailable = true; + RefPtr progressTracker = GetProgressTracker(); + progressTracker->OnImageAvailable(); + MOZ_ASSERT(progressTracker->HasImage()); + } + + if (aResult.mShouldResetCacheEntry) { + ResetCacheEntry(); + } + + if (IsDecodeRequested()) { + aResult.mImage->StartDecoding(imgIContainer::FLAG_NONE); + } +} + +bool imgRequest::ImageAvailable() const { return mImageAvailable; } + +NS_IMETHODIMP +imgRequest::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInStr, + uint64_t aOffset, uint32_t aCount) { + LOG_SCOPE_WITH_PARAM(gImgLog, "imgRequest::OnDataAvailable", "count", aCount); + + NS_ASSERTION(aRequest, "imgRequest::OnDataAvailable -- no request!"); + + RefPtr image; + RefPtr progressTracker; + bool isMultipart = false; + bool newPartPending = false; + uint64_t innerWindowId = 0; + + // Retrieve and update our state. + { + MutexAutoLock lock(mMutex); + image = mImage; + progressTracker = mProgressTracker; + isMultipart = mIsMultiPartChannel; + newPartPending = mNewPartPending; + mNewPartPending = false; + innerWindowId = mInnerWindowId; + } + + // If this is a new part, we need to sniff its content type and create an + // appropriate image. + if (newPartPending) { + NewPartResult result = + PrepareForNewPart(aRequest, aInStr, aCount, mURI, isMultipart, image, + progressTracker, innerWindowId); + bool succeeded = result.mSucceeded; + + if (result.mImage) { + image = result.mImage; + nsCOMPtr eventTarget; + + // Update our state to reflect this new part. + { + MutexAutoLock lock(mMutex); + mImage = image; + + // We only get an event target if we are not on the main thread, because + // we have to dispatch in that case. If we are on the main thread, but + // on a different scheduler group than ProgressTracker would give us, + // that is okay because nothing in imagelib requires that, just our + // listeners (which have their own checks). + if (!NS_IsMainThread()) { + eventTarget = mProgressTracker->GetEventTarget(); + MOZ_ASSERT(eventTarget); + } + + mProgressTracker = nullptr; + } + + // Some property objects are not threadsafe, and we need to send + // OnImageAvailable on the main thread, so finish on the main thread. + if (!eventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + FinishPreparingForNewPart(result); + } else { + nsCOMPtr runnable = + new FinishPreparingForNewPartRunnable(this, std::move(result)); + eventTarget->Dispatch(CreateRenderBlockingRunnable(runnable.forget()), + NS_DISPATCH_NORMAL); + } + } + + if (!succeeded) { + // Something went wrong; probably a content type issue. + Cancel(NS_IMAGELIB_ERROR_FAILURE); + return NS_BINDING_ABORTED; + } + } + + // Notify the image that it has new data. + if (aInStr) { + nsresult rv = + image->OnImageDataAvailable(aRequest, aInStr, aOffset, aCount); + + if (NS_FAILED(rv)) { + MOZ_LOG(gImgLog, LogLevel::Warning, + ("[this=%p] imgRequest::OnDataAvailable -- " + "copy to RasterImage failed\n", + this)); + Cancel(NS_IMAGELIB_ERROR_FAILURE); + return NS_BINDING_ABORTED; + } + } + + return NS_OK; +} + +void imgRequest::SetProperties(const nsACString& aContentType, + const nsACString& aContentDisposition) { + /* set our mimetype as a property */ + nsCOMPtr contentType = + do_CreateInstance("@mozilla.org/supports-cstring;1"); + if (contentType) { + contentType->SetData(aContentType); + mProperties->Set("type", contentType); + } + + /* set our content disposition as a property */ + if (!aContentDisposition.IsEmpty()) { + nsCOMPtr contentDisposition = + do_CreateInstance("@mozilla.org/supports-cstring;1"); + if (contentDisposition) { + contentDisposition->SetData(aContentDisposition); + mProperties->Set("content-disposition", contentDisposition); + } + } +} + +static nsresult sniff_mimetype_callback(nsIInputStream* in, void* data, + const char* fromRawSegment, + uint32_t toOffset, uint32_t count, + uint32_t* writeCount) { + mimetype_closure* closure = static_cast(data); + + NS_ASSERTION(closure, "closure is null!"); + + if (count > 0) { + imgLoader::GetMimeTypeFromContent(fromRawSegment, count, *closure->newType); + } + + *writeCount = 0; + return NS_ERROR_FAILURE; +} + +/** nsIInterfaceRequestor methods **/ + +NS_IMETHODIMP +imgRequest::GetInterface(const nsIID& aIID, void** aResult) { + if (!mPrevChannelSink || aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + return QueryInterface(aIID, aResult); + } + + NS_ASSERTION( + mPrevChannelSink != this, + "Infinite recursion - don't keep track of channel sinks that are us!"); + return mPrevChannelSink->GetInterface(aIID, aResult); +} + +/** nsIChannelEventSink methods **/ +NS_IMETHODIMP +imgRequest::AsyncOnChannelRedirect(nsIChannel* oldChannel, + nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + NS_ASSERTION(mRequest && mChannel, + "Got a channel redirect after we nulled out mRequest!"); + NS_ASSERTION(mChannel == oldChannel, + "Got a channel redirect for an unknown channel!"); + NS_ASSERTION(newChannel, "Got a redirect to a NULL channel!"); + + SetCacheValidation(mCacheEntry, oldChannel); + + // Prepare for callback + mRedirectCallback = callback; + mNewRedirectChannel = newChannel; + + nsCOMPtr sink(do_GetInterface(mPrevChannelSink)); + if (sink) { + nsresult rv = + sink->AsyncOnChannelRedirect(oldChannel, newChannel, flags, this); + if (NS_FAILED(rv)) { + mRedirectCallback = nullptr; + mNewRedirectChannel = nullptr; + } + return rv; + } + + (void)OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +imgRequest::OnRedirectVerifyCallback(nsresult result) { + NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback"); + NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback"); + + if (NS_FAILED(result)) { + mRedirectCallback->OnRedirectVerifyCallback(result); + mRedirectCallback = nullptr; + mNewRedirectChannel = nullptr; + return NS_OK; + } + + mChannel = mNewRedirectChannel; + mTimedChannel = do_QueryInterface(mChannel); + mNewRedirectChannel = nullptr; + + if (LOG_TEST(LogLevel::Debug)) { + LOG_MSG_WITH_PARAM(gImgLog, "imgRequest::OnChannelRedirect", "old", + mFinalURI ? mFinalURI->GetSpecOrDefault().get() : ""); + } + + // If the previous URI is a non-HTTPS URI, record that fact for later use by + // security code, which needs to know whether there is an insecure load at any + // point in the redirect chain. + bool schemeLocal = false; + if (NS_FAILED(NS_URIChainHasFlags(mFinalURI, + nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, + &schemeLocal)) || + (!mFinalURI->SchemeIs("https") && !mFinalURI->SchemeIs("chrome") && + !schemeLocal)) { + MutexAutoLock lock(mMutex); + + // The csp directive upgrade-insecure-requests performs an internal redirect + // to upgrade all requests from http to https before any data is fetched + // from the network. Do not pollute mHadInsecureRedirect in case of such an + // internal redirect. + nsCOMPtr loadInfo = mChannel->LoadInfo(); + bool upgradeInsecureRequests = + loadInfo ? loadInfo->GetUpgradeInsecureRequests() || + loadInfo->GetBrowserUpgradeInsecureRequests() + : false; + if (!upgradeInsecureRequests) { + mHadInsecureRedirect = true; + } + } + + // Update the final URI. + mChannel->GetURI(getter_AddRefs(mFinalURI)); + + if (LOG_TEST(LogLevel::Debug)) { + LOG_MSG_WITH_PARAM(gImgLog, "imgRequest::OnChannelRedirect", "new", + mFinalURI ? mFinalURI->GetSpecOrDefault().get() : ""); + } + + // Make sure we have a protocol that returns data rather than opens an + // external application, e.g. 'mailto:'. + if (nsContentUtils::IsExternalProtocol(mFinalURI)) { + mRedirectCallback->OnRedirectVerifyCallback(NS_ERROR_ABORT); + mRedirectCallback = nullptr; + return NS_OK; + } + + mRedirectCallback->OnRedirectVerifyCallback(NS_OK); + mRedirectCallback = nullptr; + return NS_OK; +} diff --git a/image/imgRequest.h b/image/imgRequest.h new file mode 100644 index 0000000000..de2d5b457c --- /dev/null +++ b/image/imgRequest.h @@ -0,0 +1,296 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_imgRequest_h +#define mozilla_image_imgRequest_h + +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIPrincipal.h" + +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Mutex.h" +#include "ImageCacheKey.h" + +class imgCacheValidator; +class imgLoader; +class imgRequestProxy; +class imgCacheEntry; +class nsIProperties; +class nsIRequest; +class nsITimedChannel; +class nsIURI; +class nsIReferrerInfo; + +namespace mozilla { +enum CORSMode : uint8_t; +namespace image { +class Image; +class ProgressTracker; +} // namespace image +} // namespace mozilla + +struct NewPartResult; + +class imgRequest final : public nsIStreamListener, + public nsIThreadRetargetableStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsIAsyncVerifyRedirectCallback { + typedef mozilla::image::Image Image; + typedef mozilla::image::ImageCacheKey ImageCacheKey; + typedef mozilla::image::ProgressTracker ProgressTracker; + typedef mozilla::dom::ReferrerPolicy ReferrerPolicy; + + public: + imgRequest(imgLoader* aLoader, const ImageCacheKey& aCacheKey); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK + + [[nodiscard]] nsresult Init(nsIURI* aURI, nsIURI* aFinalURI, + bool aHadInsecureRedirect, nsIRequest* aRequest, + nsIChannel* aChannel, imgCacheEntry* aCacheEntry, + mozilla::dom::Document* aLoadingDocument, + nsIPrincipal* aTriggeringPrincipal, + mozilla::CORSMode aCORSMode, + nsIReferrerInfo* aReferrerInfo); + + void ClearLoader(); + + // Callers must call imgRequestProxy::Notify later. + void AddProxy(imgRequestProxy* proxy); + + // Whether a given document is allowed to reuse this request without any + // revalidation. + bool CanReuseWithoutValidation(mozilla::dom::Document*) const; + + nsresult RemoveProxy(imgRequestProxy* proxy, nsresult aStatus); + + // Cancel, but also ensure that all work done in Init() is undone. Call this + // only when the channel has failed to open, and so calling Cancel() on it + // won't be sufficient. + void CancelAndAbort(nsresult aStatus); + + // Called or dispatched by cancel for main thread only execution. + void ContinueCancel(nsresult aStatus); + + // Called or dispatched by EvictFromCache for main thread only execution. + void ContinueEvict(); + + // Request that we start decoding the image as soon as data becomes available. + void StartDecoding(); + + uint64_t InnerWindowID() const; + void SetInnerWindowID(uint64_t aInnerWindowId); + + // Set the cache validation information (expiry time, whether we must + // validate, etc) on the cache entry based on the request information. + // If this function is called multiple times, the information set earliest + // wins. + static void SetCacheValidation(imgCacheEntry* aEntry, nsIRequest* aRequest); + + bool GetMultipart() const; + + // Returns whether we went through an insecure (non-HTTPS) redirect at some + // point during loading. This does not consider the final URI. + bool HadInsecureRedirect() const; + + // The CORS mode for which we loaded this image. + mozilla::CORSMode GetCORSMode() const { return mCORSMode; } + + // The ReferrerInfo in effect when loading this image. + nsIReferrerInfo* GetReferrerInfo() const { return mReferrerInfo; } + + // The principal for the document that loaded this image. Used when trying to + // validate a CORS image load. + already_AddRefed GetTriggeringPrincipal() const; + + // Return the ProgressTracker associated with this imgRequest. It may live + // in |mProgressTracker| or in |mImage.mProgressTracker|, depending on whether + // mImage has been instantiated yet. + already_AddRefed GetProgressTracker() const; + + /// Returns the Image associated with this imgRequest, if it's ready. + already_AddRefed GetImage() const; + + // Get the current principal of the image. No AddRefing. + inline nsIPrincipal* GetPrincipal() const { return mPrincipal.get(); } + + /// Get the ImageCacheKey associated with this request. + const ImageCacheKey& CacheKey() const { return mCacheKey; } + + // Resize the cache entry to 0 if it exists + void ResetCacheEntry(); + + // OK to use on any thread. + nsresult GetURI(nsIURI** aURI); + nsresult GetFinalURI(nsIURI** aURI); + bool IsChrome() const; + bool IsData() const; + + nsresult GetImageErrorCode(void); + + /// Returns a non-owning pointer to this imgRequest's MIME type. + const char* GetMimeType() const { return mContentType.get(); } + + void GetFileName(nsACString& aFileName); + + /// @return the priority of the underlying network request, or + /// PRIORITY_NORMAL if it doesn't support nsISupportsPriority. + int32_t Priority() const; + + /// Adjust the priority of the underlying network request by @aDelta on behalf + /// of @aProxy. + void AdjustPriority(imgRequestProxy* aProxy, int32_t aDelta); + + void BoostPriority(uint32_t aCategory); + + /// Returns a weak pointer to the underlying request. + nsIRequest* GetRequest() const { return mRequest; } + + nsITimedChannel* GetTimedChannel() const { return mTimedChannel; } + + imgCacheValidator* GetValidator() const { return mValidator; } + void SetValidator(imgCacheValidator* aValidator) { mValidator = aValidator; } + + void* LoadId() const { return mLoadId; } + void SetLoadId(void* aLoadId) { mLoadId = aLoadId; } + + /// Reset the cache entry after we've dropped our reference to it. Used by + /// imgLoader when our cache entry is re-requested after we've dropped our + /// reference to it. + void SetCacheEntry(imgCacheEntry* aEntry); + + /// Returns whether we've got a reference to the cache entry. + bool HasCacheEntry() const; + + /// Set whether this request is stored in the cache. If it isn't, regardless + /// of whether this request has a non-null mCacheEntry, this imgRequest won't + /// try to update or modify the image cache. + void SetIsInCache(bool aCacheable); + + void EvictFromCache(); + void RemoveFromCache(); + + // Sets properties for this image; will dispatch to main thread if needed. + void SetProperties(const nsACString& aContentType, + const nsACString& aContentDisposition); + + nsIProperties* Properties() const { return mProperties; } + + bool HasConsumers() const; + + bool ImageAvailable() const; + + bool IsDeniedCrossSiteCORSRequest() const { + return mIsDeniedCrossSiteCORSRequest; + } + + bool IsCrossSiteNoCORSRequest() const { return mIsCrossSiteNoCORSRequest; } + + private: + friend class FinishPreparingForNewPartRunnable; + + virtual ~imgRequest(); + + void FinishPreparingForNewPart(const NewPartResult& aResult); + + void Cancel(nsresult aStatus); + + // Update the cache entry size based on the image container. + void UpdateCacheEntrySize(); + + /// Returns true if StartDecoding() was called. + bool IsDecodeRequested() const; + + void AdjustPriorityInternal(int32_t aDelta); + + // Weak reference to parent loader; this request cannot outlive its owner. + imgLoader* mLoader; + nsCOMPtr mRequest; + // The original URI we were loaded with. This is the same as the URI we are + // keyed on in the cache. We store a string here to avoid off main thread + // refcounting issues with nsStandardURL. + nsCOMPtr mURI; + // The URI of the resource we ended up loading after all redirects, etc. + nsCOMPtr mFinalURI; + // The principal which triggered the load of this image. Generally either + // the principal of the document the image is being loaded into, or of the + // stylesheet which specified the image to load. Used when validating for + // CORS. + nsCOMPtr mTriggeringPrincipal; + // The principal of this image. + nsCOMPtr mPrincipal; + nsCOMPtr mProperties; + nsCOMPtr mChannel; + nsCOMPtr mPrevChannelSink; + + nsCOMPtr mTimedChannel; + + nsCString mContentType; + + /* we hold on to this to this so long as we have observers */ + RefPtr mCacheEntry; + + /// The key under which this imgRequest is stored in the image cache. + ImageCacheKey mCacheKey; + + void* mLoadId; + + /// Raw pointer to the first proxy that was added to this imgRequest. Use only + /// pointer comparisons; there's no guarantee this will remain valid. + void* mFirstProxy; + + imgCacheValidator* mValidator; + nsCOMPtr mRedirectCallback; + nsCOMPtr mNewRedirectChannel; + + // The CORS mode (defined in imgIRequest) this image was loaded with. By + // default, CORS_NONE. + mozilla::CORSMode mCORSMode; + + // The ReferrerInfo used for this image. + nsCOMPtr mReferrerInfo; + + nsresult mImageErrorCode; + + // The categories of prioritization strategy that have been requested. + uint32_t mBoostCategoriesRequested = 0; + + // If we've called OnImageAvailable. + bool mImageAvailable; + bool mIsDeniedCrossSiteCORSRequest; + bool mIsCrossSiteNoCORSRequest; + + mutable mozilla::Mutex mMutex; + + // Member variables protected by mMutex. Note that *all* flags in our bitfield + // are protected by mMutex; if you're adding a new flag that isn'protected, it + // must not be a part of this bitfield. + RefPtr mProgressTracker MOZ_GUARDED_BY(mMutex); + RefPtr mImage MOZ_GUARDED_BY(mMutex); + bool mIsMultiPartChannel : 1 MOZ_GUARDED_BY(mMutex); + bool mIsInCache : 1 MOZ_GUARDED_BY(mMutex); + bool mDecodeRequested : 1 MOZ_GUARDED_BY(mMutex); + bool mNewPartPending : 1 MOZ_GUARDED_BY(mMutex); + bool mHadInsecureRedirect : 1 MOZ_GUARDED_BY(mMutex); + // The ID of the inner window origin, used for error reporting. + uint64_t mInnerWindowId MOZ_GUARDED_BY(mMutex); +}; + +#endif // mozilla_image_imgRequest_h diff --git a/image/imgRequestProxy.cpp b/image/imgRequestProxy.cpp new file mode 100644 index 0000000000..797ae918a9 --- /dev/null +++ b/image/imgRequestProxy.cpp @@ -0,0 +1,1327 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "imgRequestProxy.h" + +#include + +#include "Image.h" +#include "ImageLogging.h" +#include "ImageOps.h" +#include "ImageTypes.h" +#include "imgINotificationObserver.h" +#include "imgLoader.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Telemetry.h" // for Telemetry +#include "mozilla/dom/DocGroup.h" // for DocGroup +#include "nsCRTGlue.h" +#include "nsError.h" + +using namespace mozilla; +using namespace mozilla::image; + +// The split of imgRequestProxy and imgRequestProxyStatic means that +// certain overridden functions need to be usable in the destructor. +// Since virtual functions can't be used in that way, this class +// provides a behavioural trait for each class to use instead. +class ProxyBehaviour { + public: + virtual ~ProxyBehaviour() = default; + + virtual already_AddRefed GetImage() const = 0; + virtual bool HasImage() const = 0; + virtual already_AddRefed GetProgressTracker() const = 0; + virtual imgRequest* GetOwner() const = 0; + virtual void SetOwner(imgRequest* aOwner) = 0; +}; + +class RequestBehaviour : public ProxyBehaviour { + public: + RequestBehaviour() : mOwner(nullptr), mOwnerHasImage(false) {} + + already_AddRefed GetImage() const override; + bool HasImage() const override; + already_AddRefed GetProgressTracker() const override; + + imgRequest* GetOwner() const override { return mOwner; } + + void SetOwner(imgRequest* aOwner) override { + mOwner = aOwner; + + if (mOwner) { + RefPtr ownerProgressTracker = GetProgressTracker(); + mOwnerHasImage = ownerProgressTracker && ownerProgressTracker->HasImage(); + } else { + mOwnerHasImage = false; + } + } + + private: + // We maintain the following invariant: + // The proxy is registered at most with a single imgRequest as an observer, + // and whenever it is, mOwner points to that object. This helps ensure that + // imgRequestProxy::~imgRequestProxy unregisters the proxy as an observer + // from whatever request it was registered with (if any). This, in turn, + // means that imgRequest::mObservers will not have any stale pointers in it. + RefPtr mOwner; + + bool mOwnerHasImage; +}; + +already_AddRefed RequestBehaviour::GetImage() const { + if (!mOwnerHasImage) { + return nullptr; + } + RefPtr progressTracker = GetProgressTracker(); + return progressTracker->GetImage(); +} + +already_AddRefed RequestBehaviour::GetProgressTracker() const { + // NOTE: It's possible that our mOwner has an Image that it didn't notify + // us about, if we were Canceled before its Image was constructed. + // (Canceling removes us as an observer, so mOwner has no way to notify us). + // That's why this method uses mOwner->GetProgressTracker() instead of just + // mOwner->mProgressTracker -- we might have a null mImage and yet have an + // mOwner with a non-null mImage (and a null mProgressTracker pointer). + return mOwner->GetProgressTracker(); +} + +NS_IMPL_ADDREF(imgRequestProxy) +NS_IMPL_RELEASE(imgRequestProxy) + +NS_INTERFACE_MAP_BEGIN(imgRequestProxy) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, PreloaderBase) + NS_INTERFACE_MAP_ENTRY(imgIRequest) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY_CONCRETE(imgRequestProxy) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsITimedChannel, TimedChannel() != nullptr) +NS_INTERFACE_MAP_END + +imgRequestProxy::imgRequestProxy() + : mBehaviour(new RequestBehaviour), + mURI(nullptr), + mListener(nullptr), + mLoadFlags(nsIRequest::LOAD_NORMAL), + mLockCount(0), + mAnimationConsumers(0), + mCancelable(true), + mCanceled(false), + mIsInLoadGroup(false), + mForceDispatchLoadGroup(false), + mListenerIsStrongRef(false), + mDecodeRequested(false), + mPendingNotify(false), + mValidating(false), + mHadListener(false), + mHadDispatch(false) { + /* member initializers and constructor code */ + LOG_FUNC(gImgLog, "imgRequestProxy::imgRequestProxy"); +} + +imgRequestProxy::~imgRequestProxy() { + /* destructor code */ + MOZ_ASSERT(!mListener, "Someone forgot to properly cancel this request!"); + + // If we had a listener, that means we would have issued notifications. With + // bug 1359833, we added support for main thread scheduler groups. Each + // imgRequestProxy may have its own associated listener, document and/or + // scheduler group. Typically most imgRequestProxy belong to the same + // document, or have no listener, which means we will want to execute all main + // thread code in that shared scheduler group. Less frequently, there may be + // multiple imgRequests and they have separate documents, which means that + // when we issue state notifications, some or all need to be dispatched to the + // appropriate scheduler group for each request. This should be rare, so we + // want to monitor the frequency of dispatching in the wild. + if (mHadListener) { + mozilla::Telemetry::Accumulate(mozilla::Telemetry::IMAGE_REQUEST_DISPATCHED, + mHadDispatch); + } + + MOZ_RELEASE_ASSERT(!mLockCount, "Someone forgot to unlock on time?"); + + ClearAnimationConsumers(); + + // Explicitly set mListener to null to ensure that the RemoveProxy + // call below can't send |this| to an arbitrary listener while |this| + // is being destroyed. This is all belt-and-suspenders in view of the + // above assert. + NullOutListener(); + + /* Call RemoveProxy with a successful status. This will keep the + channel, if still downloading data, from being canceled if 'this' is + the last observer. This allows the image to continue to download and + be cached even if no one is using it currently. + */ + mCanceled = true; + RemoveFromOwner(NS_OK); + + RemoveFromLoadGroup(); + LOG_FUNC(gImgLog, "imgRequestProxy::~imgRequestProxy"); +} + +nsresult imgRequestProxy::Init(imgRequest* aOwner, nsILoadGroup* aLoadGroup, + Document* aLoadingDocument, nsIURI* aURI, + imgINotificationObserver* aObserver) { + MOZ_ASSERT(!GetOwner() && !mListener, + "imgRequestProxy is already initialized"); + + LOG_SCOPE_WITH_PARAM(gImgLog, "imgRequestProxy::Init", "request", aOwner); + + MOZ_ASSERT(mAnimationConsumers == 0, "Cannot have animation before Init"); + + mBehaviour->SetOwner(aOwner); + mListener = aObserver; + // Make sure to addref mListener before the AddToOwner call below, since + // that call might well want to release it if the imgRequest has + // already seen OnStopRequest. + if (mListener) { + mHadListener = true; + mListenerIsStrongRef = true; + NS_ADDREF(mListener); + } + mLoadGroup = aLoadGroup; + mURI = aURI; + + // Note: AddToOwner won't send all the On* notifications immediately + AddToOwner(aLoadingDocument); + + return NS_OK; +} + +nsresult imgRequestProxy::ChangeOwner(imgRequest* aNewOwner) { + MOZ_ASSERT(GetOwner(), "Cannot ChangeOwner on a proxy without an owner!"); + + if (mCanceled) { + // Ensure that this proxy has received all notifications to date + // before we clean it up when removing it from the old owner below. + SyncNotifyListener(); + } + + // If we're holding locks, unlock the old image. + // Note that UnlockImage decrements mLockCount each time it's called. + uint32_t oldLockCount = mLockCount; + while (mLockCount) { + UnlockImage(); + } + + // If we're holding animation requests, undo them. + uint32_t oldAnimationConsumers = mAnimationConsumers; + ClearAnimationConsumers(); + + GetOwner()->RemoveProxy(this, NS_OK); + + mBehaviour->SetOwner(aNewOwner); + MOZ_ASSERT(!GetValidator(), "New owner cannot be validating!"); + + // If we were locked, apply the locks here + for (uint32_t i = 0; i < oldLockCount; i++) { + LockImage(); + } + + // If we had animation requests, restore them here. Note that we + // do this *after* RemoveProxy, which clears out animation consumers + // (see bug 601723). + for (uint32_t i = 0; i < oldAnimationConsumers; i++) { + IncrementAnimationConsumers(); + } + + AddToOwner(nullptr); + return NS_OK; +} + +NS_IMETHODIMP imgRequestProxy::GetTriggeringPrincipal( + nsIPrincipal** aTriggeringPrincipal) { + MOZ_ASSERT(GetOwner()); + nsCOMPtr triggeringPrincipal = + GetOwner()->GetTriggeringPrincipal(); + triggeringPrincipal.forget(aTriggeringPrincipal); + return NS_OK; +} + +void imgRequestProxy::MarkValidating() { + MOZ_ASSERT(GetValidator()); + mValidating = true; +} + +void imgRequestProxy::ClearValidating() { + MOZ_ASSERT(mValidating); + MOZ_ASSERT(!GetValidator()); + mValidating = false; + + // If we'd previously requested a synchronous decode, request a decode on the + // new image. + if (mDecodeRequested) { + mDecodeRequested = false; + StartDecoding(imgIContainer::FLAG_NONE); + } +} + +already_AddRefed imgRequestProxy::GetEventTarget() const { + nsCOMPtr target(mEventTarget); + return target.forget(); +} + +bool imgRequestProxy::HasDecodedPixels() { + if (IsValidating()) { + return false; + } + + RefPtr image = GetImage(); + if (image) { + return image->HasDecodedPixels(); + } + + return false; +} + +nsresult imgRequestProxy::DispatchWithTargetIfAvailable( + already_AddRefed aEvent) { + LOG_FUNC(gImgLog, "imgRequestProxy::DispatchWithTargetIfAvailable"); + + // This method should only be used when it is *expected* that we are + // dispatching an event (e.g. we want to handle an event asynchronously) + // rather we need to (e.g. we are in the wrong scheduler group context). + // As such, we do not set mHadDispatch for telemetry purposes. + if (mEventTarget) { + mEventTarget->Dispatch(CreateRenderBlockingRunnable(std::move(aEvent)), + NS_DISPATCH_NORMAL); + return NS_OK; + } + + return NS_DispatchToMainThread( + CreateRenderBlockingRunnable(std::move(aEvent))); +} + +void imgRequestProxy::AddToOwner(Document* aLoadingDocument) { + // An imgRequestProxy can be initialized with neither a listener nor a + // document. The caller could follow up later by cloning the canonical + // imgRequestProxy with the actual listener. This is possible because + // imgLoader::LoadImage does not require a valid listener to be provided. + // + // Without a listener, we don't need to set our scheduler group, because + // we have nothing to signal. However if we were told what document this + // is for, it is likely that future listeners will belong to the same + // scheduler group. + // + // With a listener, we always need to update our scheduler group. A null + // scheduler group is valid with or without a document, but that means + // we will use the most generic event target possible on dispatch. + if (aLoadingDocument) { + RefPtr docGroup = aLoadingDocument->GetDocGroup(); + if (docGroup) { + mEventTarget = docGroup->EventTargetFor(mozilla::TaskCategory::Other); + MOZ_ASSERT(mEventTarget); + } + } + + if (mListener && !mEventTarget) { + mEventTarget = do_GetMainThread(); + } + + imgRequest* owner = GetOwner(); + if (!owner) { + return; + } + + owner->AddProxy(this); +} + +void imgRequestProxy::RemoveFromOwner(nsresult aStatus) { + imgRequest* owner = GetOwner(); + if (owner) { + if (mValidating) { + imgCacheValidator* validator = owner->GetValidator(); + MOZ_ASSERT(validator); + validator->RemoveProxy(this); + mValidating = false; + } + + owner->RemoveProxy(this, aStatus); + } +} + +void imgRequestProxy::AddToLoadGroup() { + NS_ASSERTION(!mIsInLoadGroup, "Whaa, we're already in the loadgroup!"); + MOZ_ASSERT(!mForceDispatchLoadGroup); + + /* While in theory there could be a dispatch outstanding to remove this + request from the load group, in practice we only add to the load group + (when previously not in a load group) at initialization. */ + if (!mIsInLoadGroup && mLoadGroup) { + LOG_FUNC(gImgLog, "imgRequestProxy::AddToLoadGroup"); + mLoadGroup->AddRequest(this, nullptr); + mIsInLoadGroup = true; + } +} + +void imgRequestProxy::RemoveFromLoadGroup() { + if (!mIsInLoadGroup || !mLoadGroup) { + return; + } + + /* Sometimes we may not be able to remove ourselves from the load group in + the current context. This is because our listeners are not re-entrant (e.g. + we are in the middle of CancelAndForgetObserver or SyncClone). */ + if (mForceDispatchLoadGroup) { + LOG_FUNC(gImgLog, "imgRequestProxy::RemoveFromLoadGroup -- dispatch"); + + /* We take away the load group from the request temporarily; this prevents + additional dispatches via RemoveFromLoadGroup occurring, as well as + MoveToBackgroundInLoadGroup from removing and readding. This is safe + because we know that once we get here, blocking the load group at all is + unnecessary. */ + mIsInLoadGroup = false; + nsCOMPtr loadGroup = std::move(mLoadGroup); + RefPtr self(this); + DispatchWithTargetIfAvailable(NS_NewRunnableFunction( + "imgRequestProxy::RemoveFromLoadGroup", [self, loadGroup]() -> void { + loadGroup->RemoveRequest(self, nullptr, NS_OK); + })); + return; + } + + LOG_FUNC(gImgLog, "imgRequestProxy::RemoveFromLoadGroup"); + + /* calling RemoveFromLoadGroup may cause the document to finish + loading, which could result in our death. We need to make sure + that we stay alive long enough to fight another battle... at + least until we exit this function. */ + nsCOMPtr kungFuDeathGrip(this); + mLoadGroup->RemoveRequest(this, nullptr, NS_OK); + mLoadGroup = nullptr; + mIsInLoadGroup = false; +} + +void imgRequestProxy::MoveToBackgroundInLoadGroup() { + /* Even if we are still in the load group, we may have taken away the load + group reference itself because we are in the process of leaving the group. + In that case, there is no need to background the request. */ + if (!mLoadGroup) { + return; + } + + /* There is no need to dispatch if we only need to add ourselves to the load + group without removal. It is the removal which causes the problematic + callbacks (see RemoveFromLoadGroup). */ + if (mIsInLoadGroup && mForceDispatchLoadGroup) { + LOG_FUNC(gImgLog, + "imgRequestProxy::MoveToBackgroundInLoadGroup -- dispatch"); + + RefPtr self(this); + DispatchWithTargetIfAvailable(NS_NewRunnableFunction( + "imgRequestProxy::MoveToBackgroundInLoadGroup", + [self]() -> void { self->MoveToBackgroundInLoadGroup(); })); + return; + } + + LOG_FUNC(gImgLog, "imgRequestProxy::MoveToBackgroundInLoadGroup"); + nsCOMPtr kungFuDeathGrip(this); + if (mIsInLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, NS_OK); + } + + mLoadFlags |= nsIRequest::LOAD_BACKGROUND; + mLoadGroup->AddRequest(this, nullptr); +} + +/** nsIRequest / imgIRequest methods **/ + +NS_IMETHODIMP +imgRequestProxy::GetName(nsACString& aName) { + aName.Truncate(); + + if (mURI) { + mURI->GetSpec(aName); + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::IsPending(bool* _retval) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +imgRequestProxy::GetStatus(nsresult* aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP imgRequestProxy::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP imgRequestProxy::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP imgRequestProxy::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +void imgRequestProxy::SetCancelable(bool aCancelable) { + MOZ_ASSERT(NS_IsMainThread()); + mCancelable = aCancelable; +} + +NS_IMETHODIMP +imgRequestProxy::Cancel(nsresult status) { + if (mCanceled) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!mCancelable)) { + return NS_ERROR_FAILURE; + } + + LOG_SCOPE(gImgLog, "imgRequestProxy::Cancel"); + + mCanceled = true; + + nsCOMPtr ev = new imgCancelRunnable(this, status); + return DispatchWithTargetIfAvailable(ev.forget()); +} + +void imgRequestProxy::DoCancel(nsresult status) { + RemoveFromOwner(status); + RemoveFromLoadGroup(); + NullOutListener(); +} + +NS_IMETHODIMP +imgRequestProxy::CancelAndForgetObserver(nsresult aStatus) { + // If mCanceled is true but mListener is non-null, that means + // someone called Cancel() on us but the imgCancelRunnable is still + // pending. We still need to null out mListener before returning + // from this function in this case. That means we want to do the + // RemoveProxy call right now, because we need to deliver the + // onStopRequest. + if (mCanceled && !mListener) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!mCancelable)) { + MOZ_ASSERT(mCancelable, + "Shouldn't try to cancel non-cancelable requests via " + "CancelAndForgetObserver"); + return NS_ERROR_FAILURE; + } + + LOG_SCOPE(gImgLog, "imgRequestProxy::CancelAndForgetObserver"); + + mCanceled = true; + mForceDispatchLoadGroup = true; + RemoveFromOwner(aStatus); + RemoveFromLoadGroup(); + mForceDispatchLoadGroup = false; + + NullOutListener(); + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::StartDecoding(uint32_t aFlags) { + // Flag this, so we know to request after validation if pending. + if (IsValidating()) { + mDecodeRequested = true; + return NS_OK; + } + + RefPtr image = GetImage(); + if (image) { + return image->StartDecoding(aFlags); + } + + if (GetOwner()) { + GetOwner()->StartDecoding(); + } + + return NS_OK; +} + +bool imgRequestProxy::StartDecodingWithResult(uint32_t aFlags) { + // Flag this, so we know to request after validation if pending. + if (IsValidating()) { + mDecodeRequested = true; + return false; + } + + RefPtr image = GetImage(); + if (image) { + return image->StartDecodingWithResult(aFlags); + } + + if (GetOwner()) { + GetOwner()->StartDecoding(); + } + + return false; +} + +imgIContainer::DecodeResult imgRequestProxy::RequestDecodeWithResult( + uint32_t aFlags) { + if (IsValidating()) { + mDecodeRequested = true; + return imgIContainer::DECODE_REQUESTED; + } + + RefPtr image = GetImage(); + if (image) { + return image->RequestDecodeWithResult(aFlags); + } + + if (GetOwner()) { + GetOwner()->StartDecoding(); + } + + return imgIContainer::DECODE_REQUESTED; +} + +NS_IMETHODIMP +imgRequestProxy::LockImage() { + mLockCount++; + RefPtr image = + GetOwner() && GetOwner()->ImageAvailable() ? GetImage() : nullptr; + if (image) { + return image->LockImage(); + } + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::UnlockImage() { + MOZ_ASSERT(mLockCount > 0, "calling unlock but no locks!"); + + mLockCount--; + RefPtr image = + GetOwner() && GetOwner()->ImageAvailable() ? GetImage() : nullptr; + if (image) { + return image->UnlockImage(); + } + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::RequestDiscard() { + RefPtr image = GetImage(); + if (image) { + return image->RequestDiscard(); + } + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::IncrementAnimationConsumers() { + mAnimationConsumers++; + RefPtr image = + GetOwner() && GetOwner()->ImageAvailable() ? GetImage() : nullptr; + if (image) { + image->IncrementAnimationConsumers(); + } + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::DecrementAnimationConsumers() { + // We may get here if some responsible code called Increment, + // then called us, but we have meanwhile called ClearAnimationConsumers + // because we needed to get rid of them earlier (see + // imgRequest::RemoveProxy), and hence have nothing left to + // decrement. (In such a case we got rid of the animation consumers + // early, but not the observer.) + if (mAnimationConsumers > 0) { + mAnimationConsumers--; + RefPtr image = + GetOwner() && GetOwner()->ImageAvailable() ? GetImage() : nullptr; + if (image) { + image->DecrementAnimationConsumers(); + } + } + return NS_OK; +} + +void imgRequestProxy::ClearAnimationConsumers() { + while (mAnimationConsumers > 0) { + DecrementAnimationConsumers(); + } +} + +NS_IMETHODIMP +imgRequestProxy::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +imgRequestProxy::Resume() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +imgRequestProxy::GetLoadGroup(nsILoadGroup** loadGroup) { + NS_IF_ADDREF(*loadGroup = mLoadGroup.get()); + return NS_OK; +} +NS_IMETHODIMP +imgRequestProxy::SetLoadGroup(nsILoadGroup* loadGroup) { + if (loadGroup != mLoadGroup) { + MOZ_ASSERT_UNREACHABLE("Switching load groups is unsupported!"); + return NS_ERROR_NOT_IMPLEMENTED; + } + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetLoadFlags(nsLoadFlags* flags) { + *flags = mLoadFlags; + return NS_OK; +} +NS_IMETHODIMP +imgRequestProxy::SetLoadFlags(nsLoadFlags flags) { + mLoadFlags = flags; + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +imgRequestProxy::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +/** imgIRequest methods **/ + +NS_IMETHODIMP +imgRequestProxy::GetImage(imgIContainer** aImage) { + NS_ENSURE_TRUE(aImage, NS_ERROR_NULL_POINTER); + // It's possible that our owner has an image but hasn't notified us of it - + // that'll happen if we get Canceled before the owner instantiates its image + // (because Canceling unregisters us as a listener on mOwner). If we're + // in that situation, just grab the image off of mOwner. + RefPtr image = GetImage(); + nsCOMPtr imageToReturn; + if (image) { + imageToReturn = image; + } + if (!imageToReturn && GetOwner()) { + imageToReturn = GetOwner()->GetImage(); + } + if (!imageToReturn) { + return NS_ERROR_FAILURE; + } + + imageToReturn.swap(*aImage); + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetProviderId(uint32_t* aId) { + NS_ENSURE_TRUE(aId, NS_ERROR_NULL_POINTER); + + nsCOMPtr image; + nsresult rv = GetImage(getter_AddRefs(image)); + if (NS_SUCCEEDED(rv)) { + *aId = image->GetProviderId(); + } else { + *aId = 0; + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetImageStatus(uint32_t* aStatus) { + if (IsValidating()) { + // We are currently validating the image, and so our status could revert if + // we discard the cache. We should also be deferring notifications, such + // that the caller will be notified when validation completes. Rather than + // risk misleading the caller, return nothing. + *aStatus = imgIRequest::STATUS_NONE; + } else { + RefPtr progressTracker = GetProgressTracker(); + *aStatus = progressTracker->GetImageStatus(); + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetImageErrorCode(nsresult* aStatus) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + *aStatus = GetOwner()->GetImageErrorCode(); + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetURI(nsIURI** aURI) { + MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread to convert URI"); + nsCOMPtr uri = mURI; + uri.forget(aURI); + return NS_OK; +} + +nsresult imgRequestProxy::GetFinalURI(nsIURI** aURI) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + return GetOwner()->GetFinalURI(aURI); +} + +NS_IMETHODIMP +imgRequestProxy::GetNotificationObserver(imgINotificationObserver** aObserver) { + *aObserver = mListener; + NS_IF_ADDREF(*aObserver); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetMimeType(char** aMimeType) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + const char* type = GetOwner()->GetMimeType(); + if (!type) { + return NS_ERROR_FAILURE; + } + + *aMimeType = NS_xstrdup(type); + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetFileName(nsACString& aFileName) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + GetOwner()->GetFileName(aFileName); + return NS_OK; +} + +imgRequestProxy* imgRequestProxy::NewClonedProxy() { + return new imgRequestProxy(); +} + +NS_IMETHODIMP +imgRequestProxy::Clone(imgINotificationObserver* aObserver, + imgIRequest** aClone) { + nsresult result; + imgRequestProxy* proxy; + result = PerformClone(aObserver, nullptr, /* aSyncNotify */ true, &proxy); + *aClone = proxy; + return result; +} + +nsresult imgRequestProxy::SyncClone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, + imgRequestProxy** aClone) { + return PerformClone(aObserver, aLoadingDocument, + /* aSyncNotify */ true, aClone); +} + +nsresult imgRequestProxy::Clone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, + imgRequestProxy** aClone) { + return PerformClone(aObserver, aLoadingDocument, + /* aSyncNotify */ false, aClone); +} + +nsresult imgRequestProxy::PerformClone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, + bool aSyncNotify, + imgRequestProxy** aClone) { + MOZ_ASSERT(aClone, "Null out param"); + + LOG_SCOPE(gImgLog, "imgRequestProxy::Clone"); + + *aClone = nullptr; + RefPtr clone = NewClonedProxy(); + + nsCOMPtr loadGroup; + if (aLoadingDocument) { + loadGroup = aLoadingDocument->GetDocumentLoadGroup(); + } + + // It is important to call |SetLoadFlags()| before calling |Init()| because + // |Init()| adds the request to the loadgroup. + // When a request is added to a loadgroup, its load flags are merged + // with the load flags of the loadgroup. + // XXXldb That's not true anymore. Stuff from imgLoader adds the + // request to the loadgroup. + clone->SetLoadFlags(mLoadFlags); + nsresult rv = clone->Init(mBehaviour->GetOwner(), loadGroup, aLoadingDocument, + mURI, aObserver); + if (NS_FAILED(rv)) { + return rv; + } + + // Assign to *aClone before calling Notify so that if the caller expects to + // only be notified for requests it's already holding pointers to it won't be + // surprised. + NS_ADDREF(*aClone = clone); + + imgCacheValidator* validator = GetValidator(); + if (validator) { + // Note that if we have a validator, we don't want to issue notifications at + // here because we want to defer until that completes. AddProxy will add us + // to the load group; we cannot avoid that in this case, because we don't + // know when the validation will complete, and if it will cause us to + // discard our cached state anyways. We are probably already blocked by the + // original LoadImage(WithChannel) request in any event. + clone->MarkValidating(); + validator->AddProxy(clone); + } else { + // We only want to add the request to the load group of the owning document + // if it is still in progress. Some callers cannot handle a supurious load + // group removal (e.g. print preview) so we must be careful. On the other + // hand, if after cloning, the original request proxy is cancelled / + // destroyed, we need to ensure that any clones still block the load group + // if it is incomplete. + bool addToLoadGroup = mIsInLoadGroup; + if (!addToLoadGroup) { + RefPtr tracker = clone->GetProgressTracker(); + addToLoadGroup = + tracker && !(tracker->GetProgress() & FLAG_LOAD_COMPLETE); + } + + if (addToLoadGroup) { + clone->AddToLoadGroup(); + } + + if (aSyncNotify) { + // This is wrong!!! We need to notify asynchronously, but there's code + // that assumes that we don't. This will be fixed in bug 580466. Note that + // if we have a validator, we won't issue notifications anyways because + // they are deferred, so there is no point in requesting. + clone->mForceDispatchLoadGroup = true; + clone->SyncNotifyListener(); + clone->mForceDispatchLoadGroup = false; + } else { + // Without a validator, we can request asynchronous notifications + // immediately. If there was a validator, this would override the deferral + // and that would be incorrect. + clone->NotifyListener(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetImagePrincipal(nsIPrincipal** aPrincipal) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr principal = GetOwner()->GetPrincipal(); + principal.forget(aPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetHadCrossOriginRedirects(bool* aHadCrossOriginRedirects) { + *aHadCrossOriginRedirects = false; + + nsCOMPtr timedChannel = TimedChannel(); + if (timedChannel) { + bool allRedirectsSameOrigin = false; + *aHadCrossOriginRedirects = + NS_SUCCEEDED( + timedChannel->GetAllRedirectsSameOrigin(&allRedirectsSameOrigin)) && + !allRedirectsSameOrigin; + } + + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetMultipart(bool* aMultipart) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + *aMultipart = GetOwner()->GetMultipart(); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetCORSMode(int32_t* aCorsMode) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + *aCorsMode = GetOwner()->GetCORSMode(); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + if (!GetOwner()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr referrerInfo = GetOwner()->GetReferrerInfo(); + referrerInfo.forget(aReferrerInfo); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::BoostPriority(uint32_t aCategory) { + NS_ENSURE_STATE(GetOwner() && !mCanceled); + GetOwner()->BoostPriority(aCategory); + return NS_OK; +} + +/** nsISupportsPriority methods **/ + +NS_IMETHODIMP +imgRequestProxy::GetPriority(int32_t* priority) { + NS_ENSURE_STATE(GetOwner()); + *priority = GetOwner()->Priority(); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::SetPriority(int32_t priority) { + NS_ENSURE_STATE(GetOwner() && !mCanceled); + GetOwner()->AdjustPriority(this, priority - GetOwner()->Priority()); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxy::AdjustPriority(int32_t priority) { + // We don't require |!mCanceled| here. This may be called even if we're + // cancelled, because it's invoked as part of the process of removing an image + // from the load group. + NS_ENSURE_STATE(GetOwner()); + GetOwner()->AdjustPriority(this, priority); + return NS_OK; +} + +static const char* NotificationTypeToString(int32_t aType) { + switch (aType) { + case imgINotificationObserver::SIZE_AVAILABLE: + return "SIZE_AVAILABLE"; + case imgINotificationObserver::FRAME_UPDATE: + return "FRAME_UPDATE"; + case imgINotificationObserver::FRAME_COMPLETE: + return "FRAME_COMPLETE"; + case imgINotificationObserver::LOAD_COMPLETE: + return "LOAD_COMPLETE"; + case imgINotificationObserver::DECODE_COMPLETE: + return "DECODE_COMPLETE"; + case imgINotificationObserver::DISCARD: + return "DISCARD"; + case imgINotificationObserver::UNLOCKED_DRAW: + return "UNLOCKED_DRAW"; + case imgINotificationObserver::IS_ANIMATED: + return "IS_ANIMATED"; + case imgINotificationObserver::HAS_TRANSPARENCY: + return "HAS_TRANSPARENCY"; + default: + MOZ_ASSERT_UNREACHABLE("Notification list should be exhaustive"); + return "(unknown notification)"; + } +} + +void imgRequestProxy::Notify(int32_t aType, + const mozilla::gfx::IntRect* aRect) { + MOZ_ASSERT(aType != imgINotificationObserver::LOAD_COMPLETE, + "Should call OnLoadComplete"); + + LOG_FUNC_WITH_PARAM(gImgLog, "imgRequestProxy::Notify", "type", + NotificationTypeToString(aType)); + + if (!mListener || mCanceled) { + return; + } + + // Make sure the listener stays alive while we notify. + nsCOMPtr listener(mListener); + + listener->Notify(this, aType, aRect); +} + +void imgRequestProxy::OnLoadComplete(bool aLastPart) { + LOG_FUNC_WITH_PARAM(gImgLog, "imgRequestProxy::OnLoadComplete", "uri", mURI); + + // There's all sorts of stuff here that could kill us (the OnStopRequest call + // on the listener, the removal from the loadgroup, the release of the + // listener, etc). Don't let them do it. + RefPtr self(this); + + if (mListener && !mCanceled) { + // Hold a ref to the listener while we call it, just in case. + nsCOMPtr listener(mListener); + listener->Notify(this, imgINotificationObserver::LOAD_COMPLETE, nullptr); + } + + // If we're expecting more data from a multipart channel, re-add ourself + // to the loadgroup so that the document doesn't lose track of the load. + // If the request is already a background request and there's more data + // coming, we can just leave the request in the loadgroup as-is. + if (aLastPart || (mLoadFlags & nsIRequest::LOAD_BACKGROUND) == 0) { + if (aLastPart) { + RemoveFromLoadGroup(); + + nsresult errorCode = NS_OK; + // if the load is cross origin without CORS, or the CORS access is + // rejected, always fire load event to avoid leaking site information for + // . + // XXXedgar, currently we don't do the same thing for . + imgRequest* request = GetOwner(); + if (!request || !(request->IsDeniedCrossSiteCORSRequest() || + request->IsCrossSiteNoCORSRequest())) { + uint32_t status = imgIRequest::STATUS_NONE; + GetImageStatus(&status); + if (status & imgIRequest::STATUS_ERROR) { + errorCode = NS_ERROR_FAILURE; + } + } + NotifyStop(errorCode); + } else { + // More data is coming, so change the request to be a background request + // and put it back in the loadgroup. + MoveToBackgroundInLoadGroup(); + } + } + + if (mListenerIsStrongRef && aLastPart) { + MOZ_ASSERT(mListener, "How did that happen?"); + // Drop our strong ref to the listener now that we're done with + // everything. Note that this can cancel us and other fun things + // like that. Don't add anything in this method after this point. + imgINotificationObserver* obs = mListener; + mListenerIsStrongRef = false; + NS_RELEASE(obs); + } +} + +void imgRequestProxy::NullOutListener() { + // If we have animation consumers, then they don't matter anymore + if (mListener) { + ClearAnimationConsumers(); + } + + if (mListenerIsStrongRef) { + // Releasing could do weird reentery stuff, so just play it super-safe + nsCOMPtr obs; + obs.swap(mListener); + mListenerIsStrongRef = false; + } else { + mListener = nullptr; + } +} + +NS_IMETHODIMP +imgRequestProxy::GetStaticRequest(imgIRequest** aReturn) { + RefPtr proxy = + GetStaticRequest(static_cast(nullptr)); + if (proxy != this) { + RefPtr image = GetImage(); + if (image && image->HasError()) { + // image/test/unit/test_async_notification_404.js needs this, but ideally + // this special case can be removed from the scripted codepath. + return NS_ERROR_FAILURE; + } + } + proxy.forget(aReturn); + return NS_OK; +} + +already_AddRefed imgRequestProxy::GetStaticRequest( + Document* aLoadingDocument) { + MOZ_DIAGNOSTIC_ASSERT(!aLoadingDocument || + aLoadingDocument->IsStaticDocument()); + RefPtr image = GetImage(); + + bool animated; + if (!image || (NS_SUCCEEDED(image->GetAnimated(&animated)) && !animated)) { + // Early exit - we're not animated, so we don't have to do anything. + return do_AddRef(this); + } + + // We are animated. We need to create a frozen version of this image. + RefPtr frozenImage = ImageOps::Freeze(image); + + // Create a static imgRequestProxy with our new extracted frame. + nsCOMPtr currentPrincipal; + GetImagePrincipal(getter_AddRefs(currentPrincipal)); + bool hadCrossOriginRedirects = true; + GetHadCrossOriginRedirects(&hadCrossOriginRedirects); + nsCOMPtr triggeringPrincipal = GetTriggeringPrincipal(); + RefPtr req = + new imgRequestProxyStatic(frozenImage, currentPrincipal, + triggeringPrincipal, hadCrossOriginRedirects); + req->Init(nullptr, nullptr, aLoadingDocument, mURI, nullptr); + + return req.forget(); +} + +void imgRequestProxy::NotifyListener() { + // It would be nice to notify the observer directly in the status tracker + // instead of through the proxy, but there are several places we do extra + // processing when we receive notifications (like OnStopRequest()), and we + // need to check mCanceled everywhere too. + + RefPtr progressTracker = GetProgressTracker(); + if (GetOwner()) { + // Send the notifications to our listener asynchronously. + progressTracker->Notify(this); + } else { + // We don't have an imgRequest, so we can only notify the clone of our + // current state, but we still have to do that asynchronously. + MOZ_ASSERT(HasImage(), "if we have no imgRequest, we should have an Image"); + progressTracker->NotifyCurrentState(this); + } +} + +void imgRequestProxy::SyncNotifyListener() { + // It would be nice to notify the observer directly in the status tracker + // instead of through the proxy, but there are several places we do extra + // processing when we receive notifications (like OnStopRequest()), and we + // need to check mCanceled everywhere too. + + RefPtr progressTracker = GetProgressTracker(); + progressTracker->SyncNotify(this); +} + +void imgRequestProxy::SetHasImage() { + RefPtr progressTracker = GetProgressTracker(); + MOZ_ASSERT(progressTracker); + RefPtr image = progressTracker->GetImage(); + MOZ_ASSERT(image); + + // Force any private status related to the owner to reflect + // the presence of an image; + mBehaviour->SetOwner(mBehaviour->GetOwner()); + + // Apply any locks we have + for (uint32_t i = 0; i < mLockCount; ++i) { + image->LockImage(); + } + + // Apply any animation consumers we have + for (uint32_t i = 0; i < mAnimationConsumers; i++) { + image->IncrementAnimationConsumers(); + } +} + +already_AddRefed imgRequestProxy::GetProgressTracker() const { + return mBehaviour->GetProgressTracker(); +} + +already_AddRefed imgRequestProxy::GetImage() const { + return mBehaviour->GetImage(); +} + +bool RequestBehaviour::HasImage() const { + if (!mOwnerHasImage) { + return false; + } + RefPtr progressTracker = GetProgressTracker(); + return progressTracker ? progressTracker->HasImage() : false; +} + +bool imgRequestProxy::HasImage() const { return mBehaviour->HasImage(); } + +imgRequest* imgRequestProxy::GetOwner() const { return mBehaviour->GetOwner(); } + +imgCacheValidator* imgRequestProxy::GetValidator() const { + imgRequest* owner = GetOwner(); + if (!owner) { + return nullptr; + } + return owner->GetValidator(); +} + +nsITimedChannel* imgRequestProxy::TimedChannel() { + if (!GetOwner()) { + return nullptr; + } + return GetOwner()->GetTimedChannel(); +} + +////////////////// imgRequestProxyStatic methods + +class StaticBehaviour : public ProxyBehaviour { + public: + explicit StaticBehaviour(mozilla::image::Image* aImage) : mImage(aImage) {} + + already_AddRefed GetImage() const override { + RefPtr image = mImage; + return image.forget(); + } + + bool HasImage() const override { return mImage; } + + already_AddRefed GetProgressTracker() const override { + return mImage->GetProgressTracker(); + } + + imgRequest* GetOwner() const override { return nullptr; } + + void SetOwner(imgRequest* aOwner) override { + MOZ_ASSERT(!aOwner, + "We shouldn't be giving static requests a non-null owner."); + } + + private: + // Our image. We have to hold a strong reference here, because that's normally + // the job of the underlying request. + RefPtr mImage; +}; + +imgRequestProxyStatic::imgRequestProxyStatic(mozilla::image::Image* aImage, + nsIPrincipal* aImagePrincipal, + nsIPrincipal* aTriggeringPrincipal, + bool aHadCrossOriginRedirects) + : mImagePrincipal(aImagePrincipal), + mTriggeringPrincipal(aTriggeringPrincipal), + mHadCrossOriginRedirects(aHadCrossOriginRedirects) { + mBehaviour = mozilla::MakeUnique(aImage); +} + +NS_IMETHODIMP +imgRequestProxyStatic::GetImagePrincipal(nsIPrincipal** aPrincipal) { + if (!mImagePrincipal) { + return NS_ERROR_FAILURE; + } + NS_ADDREF(*aPrincipal = mImagePrincipal); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxyStatic::GetTriggeringPrincipal(nsIPrincipal** aPrincipal) { + NS_IF_ADDREF(*aPrincipal = mTriggeringPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +imgRequestProxyStatic::GetHadCrossOriginRedirects( + bool* aHadCrossOriginRedirects) { + *aHadCrossOriginRedirects = mHadCrossOriginRedirects; + return NS_OK; +} + +imgRequestProxy* imgRequestProxyStatic::NewClonedProxy() { + nsCOMPtr currentPrincipal; + GetImagePrincipal(getter_AddRefs(currentPrincipal)); + nsCOMPtr triggeringPrincipal; + GetTriggeringPrincipal(getter_AddRefs(triggeringPrincipal)); + bool hadCrossOriginRedirects = true; + GetHadCrossOriginRedirects(&hadCrossOriginRedirects); + RefPtr image = GetImage(); + return new imgRequestProxyStatic(image, currentPrincipal, triggeringPrincipal, + hadCrossOriginRedirects); +} diff --git a/image/imgRequestProxy.h b/image/imgRequestProxy.h new file mode 100644 index 0000000000..950c78341c --- /dev/null +++ b/image/imgRequestProxy.h @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_imgRequestProxy_h +#define mozilla_image_imgRequestProxy_h + +#include "imgIRequest.h" + +#include "nsIPrincipal.h" +#include "nsISupportsPriority.h" +#include "nsITimedChannel.h" +#include "nsCOMPtr.h" +#include "nsThreadUtils.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/gfx/Rect.h" + +#include "IProgressObserver.h" + +#define NS_IMGREQUESTPROXY_CID \ + { /* 20557898-1dd2-11b2-8f65-9c462ee2bc95 */ \ + 0x20557898, 0x1dd2, 0x11b2, { \ + 0x8f, 0x65, 0x9c, 0x46, 0x2e, 0xe2, 0xbc, 0x95 \ + } \ + } + +class imgCacheValidator; +class imgINotificationObserver; +class imgRequest; +class imgStatusNotifyRunnable; +class ProxyBehaviour; + +namespace mozilla { +namespace image { +class Image; +class ProgressTracker; +} // namespace image +} // namespace mozilla + +class imgRequestProxy : public mozilla::PreloaderBase, + public imgIRequest, + public mozilla::image::IProgressObserver, + public nsISupportsPriority, + public nsITimedChannel { + protected: + virtual ~imgRequestProxy(); + + public: + typedef mozilla::dom::Document Document; + typedef mozilla::image::Image Image; + typedef mozilla::image::ProgressTracker ProgressTracker; + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMGREQUESTPROXY_CID) + MOZ_DECLARE_REFCOUNTED_TYPENAME(imgRequestProxy) + NS_DECL_ISUPPORTS + NS_DECL_IMGIREQUEST + NS_DECL_NSIREQUEST + NS_DECL_NSISUPPORTSPRIORITY + // nsITimedChannel declared below + + imgRequestProxy(); + + // Callers to Init or ChangeOwner are required to call NotifyListener after + // (although not immediately after) doing so. + nsresult Init(imgRequest* aOwner, nsILoadGroup* aLoadGroup, + Document* aLoadingDocument, nsIURI* aURI, + imgINotificationObserver* aObserver); + + nsresult ChangeOwner(imgRequest* aNewOwner); // this will change mOwner. + // Do not call this if the + // previous owner has already + // sent notifications out! + + // Add the request to the load group, if any. This should only be called once + // during initialization. + void AddToLoadGroup(); + + inline bool HasObserver() const { return mListener != nullptr; } + + // Asynchronously notify this proxy's listener of the current state of the + // image, and, if we have an imgRequest mOwner, any status changes that + // happen between the time this function is called and the time the + // notification is scheduled. + void NotifyListener(); + + // Synchronously notify this proxy's listener of the current state of the + // image. Only use this function if you are currently servicing an + // asynchronously-called function. + void SyncNotifyListener(); + + // imgINotificationObserver methods: + virtual void Notify(int32_t aType, + const mozilla::gfx::IntRect* aRect = nullptr) override; + virtual void OnLoadComplete(bool aLastPart) override; + + // Other, internal-only methods: + virtual void SetHasImage() override; + + // Whether we want notifications from ProgressTracker to be deferred until + // an event it has scheduled has been fired and/or validation is complete. + virtual bool NotificationsDeferred() const override { + return IsValidating() || mPendingNotify; + } + virtual void MarkPendingNotify() override { mPendingNotify = true; } + virtual void ClearPendingNotify() override { mPendingNotify = false; } + bool IsValidating() const { return mValidating; } + void MarkValidating(); + void ClearValidating(); + + // Flags this image load as not cancelable temporarily. This is needed so that + // stylesheets can be shared across documents properly, see bug 1800979. + void SetCancelable(bool); + + already_AddRefed GetEventTarget() const override; + + // Removes all animation consumers that were created with + // IncrementAnimationConsumers. This is necessary since we need + // to do it before the proxy itself is destroyed. See + // imgRequest::RemoveProxy + void ClearAnimationConsumers(); + + nsresult SyncClone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, imgRequestProxy** aClone); + nsresult Clone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, imgRequestProxy** aClone); + already_AddRefed GetStaticRequest( + Document* aLoadingDocument); + + imgRequest* GetOwner() const; + + // PreloaderBase + // We are using the default image loader prioritization for preloads. + virtual void PrioritizeAsPreload() override {} + + protected: + friend class mozilla::image::ProgressTracker; + friend class imgStatusNotifyRunnable; + + class imgCancelRunnable; + friend class imgCancelRunnable; + + class imgCancelRunnable : public mozilla::Runnable { + public: + imgCancelRunnable(imgRequestProxy* owner, nsresult status) + : Runnable("imgCancelRunnable"), mOwner(owner), mStatus(status) {} + + NS_IMETHOD Run() override { + mOwner->DoCancel(mStatus); + return NS_OK; + } + + private: + RefPtr mOwner; + nsresult mStatus; + }; + + /* Remove from and forget the load group. */ + void RemoveFromLoadGroup(); + + /* Remove from the load group and re-add as a background request. */ + void MoveToBackgroundInLoadGroup(); + + /* Finish up canceling ourselves */ + void DoCancel(nsresult status); + + /* Do the proper refcount management to null out mListener */ + void NullOutListener(); + + // Return the ProgressTracker associated with mOwner and/or mImage. It may + // live either on mOwner or mImage, depending on whether + // (a) we have an mOwner at all + // (b) whether mOwner has instantiated its image yet + already_AddRefed GetProgressTracker() const; + + nsITimedChannel* TimedChannel(); + + already_AddRefed GetImage() const; + bool HasImage() const; + imgCacheValidator* GetValidator() const; + + nsresult PerformClone(imgINotificationObserver* aObserver, + Document* aLoadingDocument, bool aSyncNotify, + imgRequestProxy** aClone); + + virtual imgRequestProxy* NewClonedProxy(); + + public: + NS_FORWARD_SAFE_NSITIMEDCHANNEL(TimedChannel()) + + protected: + mozilla::UniquePtr mBehaviour; + + private: + friend class imgCacheValidator; + + void AddToOwner(Document* aLoadingDocument); + void RemoveFromOwner(nsresult aStatus); + + nsresult DispatchWithTargetIfAvailable(already_AddRefed aEvent); + + // The URI of our request. + nsCOMPtr mURI; + + // mListener is only promised to be a weak ref (see imgILoader.idl), + // but we actually keep a strong ref to it until we've seen our + // first OnStopRequest. + imgINotificationObserver* MOZ_UNSAFE_REF( + "Observers must call Cancel() or " + "CancelAndForgetObserver() before " + "they are destroyed") mListener; + + nsCOMPtr mLoadGroup; + nsCOMPtr mEventTarget; + + nsLoadFlags mLoadFlags; + uint32_t mLockCount; + uint32_t mAnimationConsumers; + bool mCancelable : 1; + bool mCanceled : 1; + bool mIsInLoadGroup : 1; + bool mForceDispatchLoadGroup : 1; + bool mListenerIsStrongRef : 1; + bool mDecodeRequested : 1; + + // Whether we want to defer our notifications by the non-virtual Observer + // interfaces as image loads proceed. + bool mPendingNotify : 1; + bool mValidating : 1; + bool mHadListener : 1; + bool mHadDispatch : 1; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(imgRequestProxy, NS_IMGREQUESTPROXY_CID) + +// Used for static image proxies for which no requests are available, so +// certain behaviours must be overridden to compensate. +class imgRequestProxyStatic : public imgRequestProxy { + public: + imgRequestProxyStatic(Image* aImage, nsIPrincipal* aImagePrincipal, + nsIPrincipal* aTriggeringPrincipal, + bool hadCrossOriginRedirects); + + NS_IMETHOD GetImagePrincipal(nsIPrincipal** aPrincipal) override; + NS_IMETHOD GetTriggeringPrincipal(nsIPrincipal** aPrincipal) override; + + NS_IMETHOD GetHadCrossOriginRedirects( + bool* aHadCrossOriginRedirects) override; + + protected: + imgRequestProxy* NewClonedProxy() override; + + // Our principal. We have to cache it, rather than accessing the underlying + // request on-demand, because static proxies don't have an underlying request. + const nsCOMPtr mImagePrincipal; + const nsCOMPtr mTriggeringPrincipal; + const bool mHadCrossOriginRedirects; +}; + +#endif // mozilla_image_imgRequestProxy_h diff --git a/image/imgTools.cpp b/image/imgTools.cpp new file mode 100644 index 0000000000..864efc6f44 --- /dev/null +++ b/image/imgTools.cpp @@ -0,0 +1,649 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "imgTools.h" + +#include "DecodePool.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Document.h" +#include "nsError.h" +#include "imgLoader.h" +#include "imgICache.h" +#include "imgIContainer.h" +#include "imgIEncoder.h" +#include "nsComponentManagerUtils.h" +#include "nsNetUtil.h" // for NS_NewBufferedInputStream +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsContentUtils.h" +#include "nsProxyRelease.h" +#include "nsIStreamListener.h" +#include "ImageFactory.h" +#include "Image.h" +#include "IProgressObserver.h" +#include "ScriptedNotificationObserver.h" +#include "imgIScriptedNotificationObserver.h" +#include "gfxPlatform.h" +#include "js/ArrayBuffer.h" +#include "js/RootingAPI.h" // JS::{Handle,Rooted} +#include "js/Value.h" // JS::Value +#include "Orientation.h" + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { + +namespace { + +static nsresult sniff_mimetype_callback(nsIInputStream* in, void* data, + const char* fromRawSegment, + uint32_t toOffset, uint32_t count, + uint32_t* writeCount) { + nsCString* mimeType = static_cast(data); + MOZ_ASSERT(mimeType, "mimeType is null!"); + + if (count > 0) { + imgLoader::GetMimeTypeFromContent(fromRawSegment, count, *mimeType); + } + + *writeCount = 0; + return NS_ERROR_FAILURE; +} + +class ImageDecoderListener final : public nsIStreamListener, + public IProgressObserver, + public imgIContainer { + public: + NS_DECL_ISUPPORTS + + ImageDecoderListener(nsIURI* aURI, imgIContainerCallback* aCallback, + imgINotificationObserver* aObserver) + : mURI(aURI), + mImage(nullptr), + mCallback(aCallback), + mObserver(aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD + OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) override { + if (!mImage) { + nsCOMPtr channel = do_QueryInterface(aRequest); + + nsCString mimeType; + channel->GetContentType(mimeType); + + if (aInputStream) { + // Look at the first few bytes and see if we can tell what the data is + // from that since servers tend to lie. :( + uint32_t unused; + aInputStream->ReadSegments(sniff_mimetype_callback, &mimeType, aCount, + &unused); + } + + RefPtr tracker = new ProgressTracker(); + if (mObserver) { + tracker->AddObserver(this); + } + + mImage = ImageFactory::CreateImage(channel, tracker, mimeType, mURI, + /* aIsMultiPart */ false, 0); + + if (mImage->HasError()) { + return NS_ERROR_FAILURE; + } + } + + return mImage->OnImageDataAvailable(aRequest, aInputStream, aOffset, + aCount); + } + + NS_IMETHOD + OnStartRequest(nsIRequest* aRequest) override { return NS_OK; } + + NS_IMETHOD + OnStopRequest(nsIRequest* aRequest, nsresult aStatus) override { + // Encouter a fetch error, or no data could be fetched. + if (!mImage || NS_FAILED(aStatus)) { + mCallback->OnImageReady(nullptr, mImage ? aStatus : NS_ERROR_FAILURE); + return NS_OK; + } + + mImage->OnImageDataComplete(aRequest, aStatus, true); + nsCOMPtr container = this; + mCallback->OnImageReady(container, aStatus); + return NS_OK; + } + + virtual void Notify(int32_t aType, + const nsIntRect* aRect = nullptr) override { + if (mObserver) { + mObserver->Notify(nullptr, aType, aRect); + } + } + + virtual void OnLoadComplete(bool aLastPart) override {} + + // Other notifications are ignored. + virtual void SetHasImage() override {} + virtual bool NotificationsDeferred() const override { return false; } + virtual void MarkPendingNotify() override {} + virtual void ClearPendingNotify() override {} + + // imgIContainer + NS_FORWARD_IMGICONTAINER(mImage->) + + private: + virtual ~ImageDecoderListener() = default; + + nsCOMPtr mURI; + RefPtr mImage; + nsCOMPtr mCallback; + nsCOMPtr mObserver; +}; + +NS_IMPL_ISUPPORTS(ImageDecoderListener, nsIStreamListener, imgIContainer) + +class ImageDecoderHelper final : public Runnable, + public nsIInputStreamCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + + ImageDecoderHelper(already_AddRefed aImage, + already_AddRefed aInputStream, + nsIEventTarget* aEventTarget, + imgIContainerCallback* aCallback, + nsIEventTarget* aCallbackEventTarget) + : Runnable("ImageDecoderHelper"), + mImage(std::move(aImage)), + mInputStream(std::move(aInputStream)), + mEventTarget(aEventTarget), + mCallback(aCallback), + mCallbackEventTarget(aCallbackEventTarget), + mStatus(NS_OK) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD + Run() override { + // This runnable is dispatched on the Image thread when reading data, but + // at the end, it goes back to the main-thread in order to complete the + // operation. + if (NS_IsMainThread()) { + // Let the Image know we've sent all the data. + mImage->OnImageDataComplete(nullptr, mStatus, true); + + RefPtr tracker = mImage->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + nsCOMPtr container; + if (NS_SUCCEEDED(mStatus)) { + container = mImage; + } + + mCallback->OnImageReady(container, mStatus); + return NS_OK; + } + + uint64_t length; + nsresult rv = mInputStream->Available(&length); + if (rv == NS_BASE_STREAM_CLOSED) { + return OperationCompleted(NS_OK); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return OperationCompleted(rv); + } + + // Nothing else to read, but maybe we just need to wait. + if (length == 0) { + nsCOMPtr asyncInputStream = + do_QueryInterface(mInputStream); + if (asyncInputStream) { + rv = asyncInputStream->AsyncWait(this, 0, 0, mEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return OperationCompleted(rv); + } + return NS_OK; + } + + // We really have nothing else to read. + if (length == 0) { + return OperationCompleted(NS_OK); + } + } + + // Send the source data to the Image. + rv = mImage->OnImageDataAvailable(nullptr, mInputStream, 0, + uint32_t(length)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return OperationCompleted(rv); + } + + rv = mEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return OperationCompleted(rv); + } + + return NS_OK; + } + + NS_IMETHOD + OnInputStreamReady(nsIAsyncInputStream* aAsyncInputStream) override { + MOZ_ASSERT(!NS_IsMainThread()); + return Run(); + } + + nsresult OperationCompleted(nsresult aStatus) { + MOZ_ASSERT(!NS_IsMainThread()); + + mStatus = aStatus; + mCallbackEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + return NS_OK; + } + + private: + ~ImageDecoderHelper() { + SurfaceCache::ReleaseImageOnMainThread(mImage.forget()); + NS_ReleaseOnMainThread("ImageDecoderHelper::mCallback", mCallback.forget()); + } + + RefPtr mImage; + + nsCOMPtr mInputStream; + nsCOMPtr mEventTarget; + nsCOMPtr mCallback; + nsCOMPtr mCallbackEventTarget; + + nsresult mStatus; +}; + +NS_IMPL_ISUPPORTS_INHERITED(ImageDecoderHelper, Runnable, + nsIInputStreamCallback) + +} // namespace + +/* ========== imgITools implementation ========== */ + +NS_IMPL_ISUPPORTS(imgTools, imgITools) + +imgTools::imgTools() { /* member initializers and constructor code */ +} + +imgTools::~imgTools() { /* destructor code */ +} + +NS_IMETHODIMP +imgTools::DecodeImageFromArrayBuffer(JS::Handle aArrayBuffer, + const nsACString& aMimeType, + JSContext* aCx, + imgIContainer** aContainer) { + if (!aArrayBuffer.isObject()) { + return NS_ERROR_FAILURE; + } + + JS::Rooted obj(aCx, + JS::UnwrapArrayBuffer(&aArrayBuffer.toObject())); + if (!obj) { + return NS_ERROR_FAILURE; + } + + uint8_t* bufferData = nullptr; + size_t bufferLength = 0; + bool isSharedMemory = false; + + JS::GetArrayBufferLengthAndData(obj, &bufferLength, &isSharedMemory, + &bufferData); + + // Throw for large ArrayBuffers to prevent truncation. + if (bufferLength > INT32_MAX) { + return NS_ERROR_ILLEGAL_VALUE; + } + + return DecodeImageFromBuffer((char*)bufferData, bufferLength, aMimeType, + aContainer); +} + +NS_IMETHODIMP +imgTools::DecodeImageFromBuffer(const char* aBuffer, uint32_t aSize, + const nsACString& aMimeType, + imgIContainer** aContainer) { + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_ARG_POINTER(aBuffer); + + // Create a new image container to hold the decoded data. + nsAutoCString mimeType(aMimeType); + RefPtr image = + ImageFactory::CreateAnonymousImage(mimeType, aSize); + RefPtr tracker = image->GetProgressTracker(); + + if (image->HasError()) { + return NS_ERROR_FAILURE; + } + + // Let's create a temporary inputStream. + nsCOMPtr stream; + nsresult rv = NS_NewByteInputStream( + getter_AddRefs(stream), Span(aBuffer, aSize), NS_ASSIGNMENT_DEPEND); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(stream); + MOZ_ASSERT(NS_InputStreamIsBuffered(stream)); + + rv = image->OnImageDataAvailable(nullptr, stream, 0, aSize); + NS_ENSURE_SUCCESS(rv, rv); + + // Let the Image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + NS_ENSURE_SUCCESS(rv, rv); + + // All done. + image.forget(aContainer); + return NS_OK; +} + +NS_IMETHODIMP +imgTools::DecodeImageFromChannelAsync(nsIURI* aURI, nsIChannel* aChannel, + imgIContainerCallback* aCallback, + imgINotificationObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aChannel); + NS_ENSURE_ARG_POINTER(aCallback); + + RefPtr listener = + new ImageDecoderListener(aURI, aCallback, aObserver); + + return aChannel->AsyncOpen(listener); +} + +NS_IMETHODIMP +imgTools::DecodeImageAsync(nsIInputStream* aInStr, const nsACString& aMimeType, + imgIContainerCallback* aCallback, + nsIEventTarget* aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_ARG_POINTER(aInStr); + NS_ENSURE_ARG_POINTER(aCallback); + NS_ENSURE_ARG_POINTER(aEventTarget); + + nsresult rv; + + // Let's continuing the reading on a separate thread. + DecodePool* decodePool = DecodePool::Singleton(); + MOZ_ASSERT(decodePool); + + RefPtr target = decodePool->GetIOEventTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_FAILURE); + + // Prepare the input stream. + nsCOMPtr stream = aInStr; + if (!NS_InputStreamIsBuffered(aInStr)) { + nsCOMPtr bufStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream), stream.forget(), + 1024); + NS_ENSURE_SUCCESS(rv, rv); + stream = std::move(bufStream); + } + + // Create a new image container to hold the decoded data. + nsAutoCString mimeType(aMimeType); + RefPtr image = ImageFactory::CreateAnonymousImage(mimeType, 0); + + // Already an error? + if (image->HasError()) { + return NS_ERROR_FAILURE; + } + + RefPtr helper = new ImageDecoderHelper( + image.forget(), stream.forget(), target, aCallback, aEventTarget); + rv = target->Dispatch(helper.forget(), NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * This takes a DataSourceSurface rather than a SourceSurface because some + * of the callers have a DataSourceSurface and we don't want to call + * GetDataSurface on such surfaces since that may incur a conversion to + * SurfaceType::DATA which we don't need. + */ +static nsresult EncodeImageData(DataSourceSurface* aDataSurface, + DataSourceSurface::ScopedMap& aMap, + const nsACString& aMimeType, + const nsAString& aOutputOptions, + nsIInputStream** aStream) { + MOZ_ASSERT(aDataSurface->GetFormat() == SurfaceFormat::B8G8R8A8 || + aDataSurface->GetFormat() == SurfaceFormat::B8G8R8X8, + "We're assuming B8G8R8A8/X8"); + + // Get an image encoder for the media type + nsAutoCString encoderCID("@mozilla.org/image/encoder;2?type="_ns + aMimeType); + + nsCOMPtr encoder = do_CreateInstance(encoderCID.get()); + if (!encoder) { + return NS_IMAGELIB_ERROR_NO_ENCODER; + } + + IntSize size = aDataSurface->GetSize(); + uint32_t dataLength = aMap.GetStride() * size.height; + + // Encode the bitmap + nsresult rv = encoder->InitFromData( + aMap.GetData(), dataLength, size.width, size.height, aMap.GetStride(), + imgIEncoder::INPUT_FORMAT_HOSTARGB, aOutputOptions); + NS_ENSURE_SUCCESS(rv, rv); + + encoder.forget(aStream); + return NS_OK; +} + +static nsresult EncodeImageData(DataSourceSurface* aDataSurface, + const nsACString& aMimeType, + const nsAString& aOutputOptions, + nsIInputStream** aStream) { + DataSourceSurface::ScopedMap map(aDataSurface, DataSourceSurface::READ); + if (!map.IsMapped()) { + return NS_ERROR_FAILURE; + } + + return EncodeImageData(aDataSurface, map, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP +imgTools::EncodeImage(imgIContainer* aContainer, const nsACString& aMimeType, + const nsAString& aOutputOptions, + nsIInputStream** aStream) { + // Use frame 0 from the image container. + RefPtr frame = aContainer->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + RefPtr dataSurface; + + if (frame->GetFormat() == SurfaceFormat::B8G8R8A8 || + frame->GetFormat() == SurfaceFormat::B8G8R8X8) { + dataSurface = frame->GetDataSurface(); + } else { + // Convert format to SurfaceFormat::B8G8R8A8 + dataSurface = gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat( + frame, SurfaceFormat::B8G8R8A8); + } + + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + + return EncodeImageData(dataSurface, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP +imgTools::EncodeScaledImage(imgIContainer* aContainer, + const nsACString& aMimeType, int32_t aScaledWidth, + int32_t aScaledHeight, + const nsAString& aOutputOptions, + nsIInputStream** aStream) { + NS_ENSURE_ARG(aScaledWidth >= 0 && aScaledHeight >= 0); + + // If no scaled size is specified, we'll just encode the image at its + // original size (no scaling). + if (aScaledWidth == 0 && aScaledHeight == 0) { + return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream); + } + + // Retrieve the image's size. + int32_t imageWidth = 0; + int32_t imageHeight = 0; + aContainer->GetWidth(&imageWidth); + aContainer->GetHeight(&imageHeight); + + // If the given width or height is zero we'll replace it with the image's + // original dimensions. + IntSize scaledSize(aScaledWidth == 0 ? imageWidth : aScaledWidth, + aScaledHeight == 0 ? imageHeight : aScaledHeight); + + // Use frame 0 from the image container. + RefPtr frame = aContainer->GetFrameAtSize( + scaledSize, imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_HIGH_QUALITY_SCALING | + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + // If the given surface is the right size/format, we can encode it directly. + if (scaledSize == frame->GetSize() && + (frame->GetFormat() == SurfaceFormat::B8G8R8A8 || + frame->GetFormat() == SurfaceFormat::B8G8R8X8)) { + RefPtr dataSurface = frame->GetDataSurface(); + if (dataSurface) { + return EncodeImageData(dataSurface, aMimeType, aOutputOptions, aStream); + } + } + + // Otherwise we need to scale it using a draw target. + RefPtr dataSurface = + Factory::CreateDataSourceSurface(scaledSize, SurfaceFormat::B8G8R8A8); + if (NS_WARN_IF(!dataSurface)) { + return NS_ERROR_FAILURE; + } + + DataSourceSurface::ScopedMap map(dataSurface, DataSourceSurface::READ_WRITE); + if (!map.IsMapped()) { + return NS_ERROR_FAILURE; + } + + RefPtr dt = Factory::CreateDrawTargetForData( + BackendType::SKIA, map.GetData(), dataSurface->GetSize(), map.GetStride(), + SurfaceFormat::B8G8R8A8); + if (!dt) { + gfxWarning() << "imgTools::EncodeImage failed in CreateDrawTargetForData"; + return NS_ERROR_OUT_OF_MEMORY; + } + + IntSize frameSize = frame->GetSize(); + dt->DrawSurface(frame, Rect(0, 0, scaledSize.width, scaledSize.height), + Rect(0, 0, frameSize.width, frameSize.height), + DrawSurfaceOptions(), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + + return EncodeImageData(dataSurface, map, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP +imgTools::EncodeCroppedImage(imgIContainer* aContainer, + const nsACString& aMimeType, int32_t aOffsetX, + int32_t aOffsetY, int32_t aWidth, int32_t aHeight, + const nsAString& aOutputOptions, + nsIInputStream** aStream) { + NS_ENSURE_ARG(aOffsetX >= 0 && aOffsetY >= 0 && aWidth >= 0 && aHeight >= 0); + + // Offsets must be zero when no width and height are given or else we're out + // of bounds. + NS_ENSURE_ARG(aWidth + aHeight > 0 || aOffsetX + aOffsetY == 0); + + // If no size is specified then we'll preserve the image's original dimensions + // and don't need to crop. + if (aWidth == 0 && aHeight == 0) { + return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream); + } + + // Use frame 0 from the image container. + RefPtr frame = aContainer->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + int32_t frameWidth = frame->GetSize().width; + int32_t frameHeight = frame->GetSize().height; + + // If the given width or height is zero we'll replace it with the image's + // original dimensions. + if (aWidth == 0) { + aWidth = frameWidth; + } else if (aHeight == 0) { + aHeight = frameHeight; + } + + // Check that the given crop rectangle is within image bounds. + NS_ENSURE_ARG(frameWidth >= aOffsetX + aWidth && + frameHeight >= aOffsetY + aHeight); + + RefPtr dataSurface = Factory::CreateDataSourceSurface( + IntSize(aWidth, aHeight), SurfaceFormat::B8G8R8A8, + /* aZero = */ true); + if (NS_WARN_IF(!dataSurface)) { + return NS_ERROR_FAILURE; + } + + DataSourceSurface::ScopedMap map(dataSurface, DataSourceSurface::READ_WRITE); + if (!map.IsMapped()) { + return NS_ERROR_FAILURE; + } + + RefPtr dt = Factory::CreateDrawTargetForData( + BackendType::SKIA, map.GetData(), dataSurface->GetSize(), map.GetStride(), + SurfaceFormat::B8G8R8A8); + if (!dt) { + gfxWarning() + << "imgTools::EncodeCroppedImage failed in CreateDrawTargetForData"; + return NS_ERROR_OUT_OF_MEMORY; + } + dt->CopySurface(frame, IntRect(aOffsetX, aOffsetY, aWidth, aHeight), + IntPoint(0, 0)); + + return EncodeImageData(dataSurface, map, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP +imgTools::CreateScriptedObserver(imgIScriptedNotificationObserver* aInner, + imgINotificationObserver** aObserver) { + NS_ADDREF(*aObserver = new ScriptedNotificationObserver(aInner)); + return NS_OK; +} + +NS_IMETHODIMP +imgTools::GetImgLoaderForDocument(dom::Document* aDoc, imgILoader** aLoader) { + NS_IF_ADDREF(*aLoader = nsContentUtils::GetImgLoaderForDocument(aDoc)); + return NS_OK; +} + +NS_IMETHODIMP +imgTools::GetImgCacheForDocument(dom::Document* aDoc, imgICache** aCache) { + nsCOMPtr loader; + nsresult rv = GetImgLoaderForDocument(aDoc, getter_AddRefs(loader)); + NS_ENSURE_SUCCESS(rv, rv); + return CallQueryInterface(loader, aCache); +} + +} // namespace image +} // namespace mozilla diff --git a/image/imgTools.h b/image/imgTools.h new file mode 100644 index 0000000000..1075e1dbef --- /dev/null +++ b/image/imgTools.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_imgITools_h +#define mozilla_image_imgITools_h + +#include "imgITools.h" + +#define NS_IMGTOOLS_CID \ + { /* 3d8fa16d-c9e1-4b50-bdef-2c7ae249967a */ \ + 0x3d8fa16d, 0xc9e1, 0x4b50, { \ + 0xbd, 0xef, 0x2c, 0x7a, 0xe2, 0x49, 0x96, 0x7a \ + } \ + } + +namespace mozilla { +namespace image { + +class imgTools final : public imgITools { + public: + NS_DECL_ISUPPORTS + NS_DECL_IMGITOOLS + + imgTools(); + + private: + virtual ~imgTools(); +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_imgITools_h diff --git a/image/moz.build b/image/moz.build new file mode 100644 index 0000000000..aab71c1362 --- /dev/null +++ b/image/moz.build @@ -0,0 +1,139 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["build", "decoders", "encoders"] +if CONFIG["ENABLE_TESTS"]: + DIRS += ["test/gtest"] + +if CONFIG["FUZZING_INTERFACES"]: + DIRS += ["test/fuzzing"] + +with Files("**"): + BUG_COMPONENT = ("Core", "Graphics: ImageLib") + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.ini", + "test/browser/browser_sandbox_headless.ini", +] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +MOCHITEST_CHROME_MANIFESTS += ["test/mochitest/chrome.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +XPIDL_SOURCES += [ + "imgICache.idl", + "imgIContainer.idl", + "imgIContainerDebug.idl", + "imgIEncoder.idl", + "imgILoader.idl", + "imgINotificationObserver.idl", + "imgIRequest.idl", + "imgIScriptedNotificationObserver.idl", + "imgITools.idl", + "nsIIconURI.idl", +] + +XPIDL_MODULE = "imglib2" + +EXPORTS += [ + "FrameTimeout.h", + "ImageBlocker.h", + "ImageCacheKey.h", + "ImageLogging.h", + "ImageMetadata.h", + "ImageOps.h", + "ImageRegion.h", + "ImgDrawResult.h", + "imgLoader.h", + "imgRequest.h", + "imgRequestProxy.h", + "IProgressObserver.h", + "Orientation.h", + "SurfaceCacheUtils.h", +] + +EXPORTS.mozilla.image += [ + "encoders/bmp/nsBMPEncoder.h", + "encoders/ico/nsICOEncoder.h", + "encoders/jpeg/nsJPEGEncoder.h", + "encoders/png/nsPNGEncoder.h", + "ICOFileHeaders.h", + "ImageMemoryReporter.h", + "Resolution.h", + "WebRenderImageProvider.h", +] + +UNIFIED_SOURCES += [ + "AnimationFrameBuffer.cpp", + "AnimationSurfaceProvider.cpp", + "BlobSurfaceProvider.cpp", + "ClippedImage.cpp", + "DecodedSurfaceProvider.cpp", + "Decoder.cpp", + "DecoderFactory.cpp", + "DynamicImage.cpp", + "FrameAnimator.cpp", + "FrozenImage.cpp", + "IDecodingTask.cpp", + "Image.cpp", + "ImageBlocker.cpp", + "ImageCacheKey.cpp", + "ImageFactory.cpp", + "ImageMemoryReporter.cpp", + "ImageOps.cpp", + "ImageWrapper.cpp", + "imgFrame.cpp", + "imgLoader.cpp", + "imgRequest.cpp", + "imgRequestProxy.cpp", + "imgTools.cpp", + "MultipartImage.cpp", + "OrientedImage.cpp", + "ProgressTracker.cpp", + "RasterImage.cpp", + "ScriptedNotificationObserver.cpp", + "ShutdownTracker.cpp", + "SourceBuffer.cpp", + "SurfaceCache.cpp", + "SurfaceCacheUtils.cpp", + "SurfacePipe.cpp", + "SVGDocumentWrapper.cpp", + "VectorImage.cpp", +] + +UNIFIED_SOURCES += ["Downscaler.cpp"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + SOURCES += ["DecodePool.cpp"] +else: + UNIFIED_SOURCES += ["DecodePool.cpp"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + # Because SVGDocumentWrapper.cpp includes "mozilla/dom/SVGSVGElement.h" + "/dom/base", + "/dom/svg", + # Because imgFrame.cpp includes "cairo.h" + "/gfx/cairo/cairo/src", + # We need to instantiate the decoders + "/image/decoders", + # For URI-related functionality + "/netwerk/base", + # For nsHttpChannel.h + "/netwerk/protocol/http", + # DecodePool uses thread-related facilities. + "/xpcom/threads", +] + +LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"] + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/image/nsIIconURI.idl b/image/nsIIconURI.idl new file mode 100644 index 0000000000..b55ee8d8c7 --- /dev/null +++ b/image/nsIIconURI.idl @@ -0,0 +1,92 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIURL.idl" + + /** + * nsIIconURI + * + * This interface derives from nsIURI, to provide additional information + * about moz-icon URIs. + * + * What *is* a moz-icon URI you ask? Well, it has the following syntax: + * + * moz-icon:[ | // | //stock/]? + * ['?'[]] + * + * is a valid URL spec. + * + * is any filename with an extension, e.g. "dummy.html". + * If the file you want an icon for isn't known to exist, you can use this + * instead of a URL and just place a dummy file name with the extension or + * content type you want. + * + * is the name of a platform-dependant stock icon. + * + * Legal parameter value pairs are listed below: + * + * Parameter: size + * Values: [ | button | toolbar | toolbarsmall | menu | + * dialog] + * Description: If integer, this is the desired size in square pixels of + * the icon + * Else, use the OS default for the specified keyword context. + * + * Parameter: state + * Values: [normal | disabled] + * Description: The state of the icon. + * + * Parameter: contentType + * Values: + * Description: The mime type we want an icon for. This is ignored by + * stock images. + */ + +[scriptable, builtinclass, uuid(f8fe5ef2-5f2b-43f3-857d-5b64d192c427)] +interface nsIMozIconURI : nsIURI +{ + /// iconFile: the file URL contained within this moz-icon url, or null. + readonly attribute nsIURL iconURL; + + /// imageSize: The image area in square pixels, defaults to 16 if unspecified. + readonly attribute unsigned long imageSize; + + /// stockIcon: The stock icon name requested from the OS. + readonly attribute ACString stockIcon; + + /// iconSize: The stock icon size requested from the OS. + readonly attribute ACString iconSize; + + /// iconState: The stock icon state requested from the OS. + readonly attribute ACString iconState; + + /// contentType: A valid mime type, or the empty string. + readonly attribute ACString contentType; + + /// fileExtension: The file extension of the file which we are looking up. + readonly attribute ACString fileExtension; +}; + +%{C++ + +// CID for nsMozIconURI, if implemented on this platform. +#define NS_MOZICONURI_CID \ +{ \ + 0x43a88e0e, \ + 0x2d37, \ + 0x11d5, \ + { 0x99, 0x7, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b } \ +} + +#define NS_MOZICONURIMUTATOR_CID \ +{ \ + 0x1460df3b, \ + 0x774c, \ + 0x4205, \ + {0x83, 0x49, 0x83, 0x8e, 0x50, 0x7c, 0x3e, 0xf9} \ +} + +%} diff --git a/image/test/browser/animated.gif b/image/test/browser/animated.gif new file mode 100644 index 0000000000..eb034e1501 Binary files /dev/null and b/image/test/browser/animated.gif differ diff --git a/image/test/browser/animated2.gif b/image/test/browser/animated2.gif new file mode 100644 index 0000000000..053eaae688 Binary files /dev/null and b/image/test/browser/animated2.gif differ diff --git a/image/test/browser/big.png b/image/test/browser/big.png new file mode 100644 index 0000000000..94e7eb6db2 Binary files /dev/null and b/image/test/browser/big.png differ diff --git a/image/test/browser/browser.ini b/image/test/browser/browser.ini new file mode 100644 index 0000000000..b5ba8581a3 --- /dev/null +++ b/image/test/browser/browser.ini @@ -0,0 +1,20 @@ +[DEFAULT] +support-files = + animated.gif + animated2.gif + big.png + head.js + image.html + imageX2.html + browser_docshell_type_editor/** + +[browser_bug666317.js] +skip-if = true # Bug 1207012 - Permaorange from an uncaught exception that isn't actually turning the suite orange until it hits beta, Bug 948194 - Decoded Images seem to not be discarded on memory-pressure notification +[browser_image.js] +skip-if = true # Bug 987616 +[browser_docshell_type_editor.js] +[browser_offscreen_image_in_out_of_process_iframe.js] +https_first_disabled = true +support-files = + empty.html +[browser_mozicon_file.js] diff --git a/image/test/browser/browser_bug666317.js b/image/test/browser/browser_bug666317.js new file mode 100644 index 0000000000..7f58c61c56 --- /dev/null +++ b/image/test/browser/browser_bug666317.js @@ -0,0 +1,138 @@ +waitForExplicitFinish(); + +var pageSource = + "" + + '' + + ""; + +var oldDiscardingPref, oldTab, newTab; +var prefBranch = Services.prefs.getBranch("image.mem."); + +var gWaitingForDiscard = false; +var gScriptedObserver; +var gClonedRequest; + +function ImageObserver(decodeCallback, discardCallback) { + this.decodeComplete = function onDecodeComplete(aRequest) { + decodeCallback(); + }; + + this.discard = function onDiscard(request) { + if (!gWaitingForDiscard) { + return; + } + + this.synchronous = false; + discardCallback(); + }; + + this.synchronous = true; +} + +function currentRequest() { + let img = gBrowser + .getBrowserForTab(newTab) + .contentWindow.document.getElementById("testImg"); + return img.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); +} + +function isImgDecoded() { + let request = currentRequest(); + return !!(request.imageStatus & Ci.imgIRequest.STATUS_DECODE_COMPLETE); +} + +// Ensure that the image is decoded by drawing it to a canvas. +function forceDecodeImg() { + let doc = gBrowser.getBrowserForTab(newTab).contentWindow.document; + let img = doc.getElementById("testImg"); + let canvas = doc.createElement("canvas"); + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); +} + +function runAfterAsyncEvents(aCallback) { + function handlePostMessage(aEvent) { + if (aEvent.data == "next") { + window.removeEventListener("message", handlePostMessage); + aCallback(); + } + } + + window.addEventListener("message", handlePostMessage); + + // We'll receive the 'message' event after everything else that's currently in + // the event queue (which is a stronger guarantee than setTimeout, because + // setTimeout events may be coalesced). This lets us ensure that we run + // aCallback *after* any asynchronous events are delivered. + window.postMessage("next", "*"); +} + +function test() { + // Enable the discarding pref. + oldDiscardingPref = prefBranch.getBoolPref("discardable"); + prefBranch.setBoolPref("discardable", true); + + // Create and focus a new tab. + oldTab = gBrowser.selectedTab; + newTab = BrowserTestUtils.addTab(gBrowser, "data:text/html," + pageSource); + gBrowser.selectedTab = newTab; + + // Run step2 after the tab loads. + gBrowser.getBrowserForTab(newTab).addEventListener("pageshow", step2); +} + +function step2() { + // Create the image observer. + var observer = new ImageObserver( + () => runAfterAsyncEvents(step3), // DECODE_COMPLETE + () => runAfterAsyncEvents(step5) + ); // DISCARD + gScriptedObserver = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(observer); + + // Clone the current imgIRequest with our new observer. + var request = currentRequest(); + gClonedRequest = request.clone(gScriptedObserver); + + // Check that the image is decoded. + forceDecodeImg(); + + // The DECODE_COMPLETE notification is delivered asynchronously. ImageObserver will + // eventually call step3. +} + +function step3() { + ok(isImgDecoded(), "Image should initially be decoded."); + + // Focus the old tab, then fire a memory-pressure notification. This should + // cause the decoded image in the new tab to be discarded. + gBrowser.selectedTab = oldTab; + + // Allow time to process the tab change. + runAfterAsyncEvents(step4); +} + +function step4() { + gWaitingForDiscard = true; + + var os = Services.obs; + os.notifyObservers(null, "memory-pressure", "heap-minimize"); + + // The DISCARD notification is delivered asynchronously. ImageObserver will + // eventually call step5. (Or else, sadly, the test will time out.) +} + +function step5() { + ok(true, "Image should be discarded."); + + // And we're done. + gBrowser.removeTab(newTab); + prefBranch.setBoolPref("discardable", oldDiscardingPref); + + gClonedRequest.cancelAndForgetObserver(0); + + finish(); +} diff --git a/image/test/browser/browser_docshell_type_editor.js b/image/test/browser/browser_docshell_type_editor.js new file mode 100644 index 0000000000..baa89c0f07 --- /dev/null +++ b/image/test/browser/browser_docshell_type_editor.js @@ -0,0 +1,134 @@ +"use strict"; + +const SIMPLE_HTML = "data:text/html,"; + +/** + * Returns the directory where the chrome.manifest file for the test can be found. + * + * @return nsIFile of the manifest directory + */ +function getManifestDir() { + let path = getTestFilePath("browser_docshell_type_editor"); + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + return file; +} + +// The following URI is *not* accessible to content, hence loading that URI +// from an unprivileged site should be blocked. If docshell is of appType +// APP_TYPE_EDITOR however the load should be allowed. +// >> chrome://test1/skin/privileged.png + +add_task(async function () { + info("docshell of appType APP_TYPE_EDITOR can access privileged images."); + + // Load a temporary manifest adding a route to a privileged image + let manifestDir = getManifestDir(); + Components.manager.addBootstrappedManifestLocation(manifestDir); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SIMPLE_HTML, + }, + async function (browser) { + await SpecialPowers.spawn(browser, [], async function () { + let rootDocShell = docShell.sameTypeRootTreeItem.QueryInterface( + Ci.nsIDocShell + ); + let defaultAppType = rootDocShell.appType; + + rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_EDITOR; + + is( + rootDocShell.appType, + Ci.nsIDocShell.APP_TYPE_EDITOR, + "sanity check: appType after update should be type editor" + ); + + return new Promise(resolve => { + let doc = content.document; + let image = doc.createElement("img"); + image.onload = function () { + ok(true, "APP_TYPE_EDITOR is allowed to load privileged image"); + // restore appType of rootDocShell before moving on to the next test + rootDocShell.appType = defaultAppType; + resolve(); + }; + image.onerror = function () { + ok(false, "APP_TYPE_EDITOR is allowed to load privileged image"); + // restore appType of rootDocShell before moving on to the next test + rootDocShell.appType = defaultAppType; + resolve(); + }; + doc.body.appendChild(image); + image.src = "chrome://test1/skin/privileged.png"; + }); + }); + } + ); + + Components.manager.removeBootstrappedManifestLocation(manifestDir); +}); + +add_task(async function () { + info( + "docshell of appType APP_TYPE_UNKNOWN can *not* access privileged images." + ); + + // Load a temporary manifest adding a route to a privileged image + let manifestDir = getManifestDir(); + Components.manager.addBootstrappedManifestLocation(manifestDir); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SIMPLE_HTML, + }, + async function (browser) { + await SpecialPowers.spawn(browser, [], async function () { + let rootDocShell = docShell.sameTypeRootTreeItem.QueryInterface( + Ci.nsIDocShell + ); + let defaultAppType = rootDocShell.appType; + + rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_UNKNOWN; + + is( + rootDocShell.appType, + Ci.nsIDocShell.APP_TYPE_UNKNOWN, + "sanity check: appType of docshell should be unknown" + ); + + return new Promise(resolve => { + let doc = content.document; + let image = doc.createElement("img"); + image.onload = function () { + ok( + false, + "APP_TYPE_UNKNOWN is *not* allowed to access privileged image" + ); + // restore appType of rootDocShell before moving on to the next test + rootDocShell.appType = defaultAppType; + resolve(); + }; + image.onerror = function () { + ok( + true, + "APP_TYPE_UNKNOWN is *not* allowed to access privileged image" + ); + // restore appType of rootDocShell before moving on to the next test + rootDocShell.appType = defaultAppType; + resolve(); + }; + doc.body.appendChild(image); + // Set the src via wrappedJSObject so the load is triggered with + // the content page's principal rather than ours. + image.wrappedJSObject.src = "chrome://test1/skin/privileged.png"; + }); + }); + } + ); + + Components.manager.removeBootstrappedManifestLocation(manifestDir); +}); diff --git a/image/test/browser/browser_docshell_type_editor/chrome.manifest b/image/test/browser/browser_docshell_type_editor/chrome.manifest new file mode 100644 index 0000000000..85510a8af9 --- /dev/null +++ b/image/test/browser/browser_docshell_type_editor/chrome.manifest @@ -0,0 +1 @@ +skin test1 test img/ \ No newline at end of file diff --git a/image/test/browser/browser_docshell_type_editor/img/privileged.png b/image/test/browser/browser_docshell_type_editor/img/privileged.png new file mode 100644 index 0000000000..2bf7b7e828 Binary files /dev/null and b/image/test/browser/browser_docshell_type_editor/img/privileged.png differ diff --git a/image/test/browser/browser_image.js b/image/test/browser/browser_image.js new file mode 100644 index 0000000000..0ae55df640 --- /dev/null +++ b/image/test/browser/browser_image.js @@ -0,0 +1,261 @@ +waitForExplicitFinish(); +requestLongerTimeout(2); // see bug 660123 -- this test is slow on Mac. + +// A hold on the current timer, so it doesn't get GCed out from +// under us +var gTimer; + +// Browsing to a new URL - pushing us into the bfcache - should cause +// animations to stop, and resume when we return +/* global yield */ +function testBFCache() { + function theTest() { + var abort = false; + var chances, gImage, gFrames; + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + TESTROOT + "image.html" + ); + gBrowser.selectedBrowser.addEventListener( + "pageshow", + function () { + var window = gBrowser.contentWindow; + // If false, we are in an optimized build, and we abort this and + // all further tests + if ( + !actOnMozImage(window.document, "img1", function (image) { + gImage = image; + gFrames = gImage.framesNotified; + }) + ) { + gBrowser.removeCurrentTab(); + abort = true; + } + goer.next(); + }, + { capture: true, once: true } + ); + yield; + if (abort) { + finish(); + yield; // optimized build + } + + // Let animation run for a bit + chances = 120; + do { + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + if (gImage.framesNotified >= 20) { + goer.send(true); + } else { + chances--; + goer.send(chances == 0); // maybe if we wait a bit, it will happen + } + }, + 500, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } while (!yield); + is(chances > 0, true, "Must have animated a few frames so far"); + + // Browse elsewhere; push our animating page into the bfcache + gBrowser.loadURI(Services.io.newURI("about:blank")); + + // Wait a bit for page to fully load, then wait a while and + // see that no animation occurs. + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + gFrames = gImage.framesNotified; + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + // Might have a few stray frames, until other page totally loads + var additionalFrames = gImage.framesNotified - gFrames; + is( + additionalFrames == 0, + true, + "Must have not animated in bfcache! Got " + + additionalFrames + + " additional frames" + ); + goer.next(); + }, + 4000, + Ci.nsITimer.TYPE_ONE_SHOT + ); // 4 seconds - expect 40 frames + }, + 0, + Ci.nsITimer.TYPE_ONE_SHOT + ); // delay of 0 - wait for next event loop + yield; + + // Go back + gBrowser.goBack(); + + chances = 120; + do { + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + if (gImage.framesNotified - gFrames >= 20) { + goer.send(true); + } else { + chances--; + goer.send(chances == 0); // maybe if we wait a bit, it will happen + } + }, + 500, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } while (!yield); + is(chances > 0, true, "Must have animated once out of bfcache!"); + + // Finally, check that the css background image has essentially the same + // # of frames, implying that it animated at the same times as the regular + // image. We can easily retrieve regular images through their HTML image + // elements, which is what we did before. For the background image, we + // create a regular image now, and read the current frame count. + var doc = gBrowser.selectedBrowser.contentWindow.document; + var div = doc.getElementById("background_div"); + div.innerHTML += ''; + actOnMozImage(doc, "img3", function (image) { + is( + Math.abs(image.framesNotified - gImage.framesNotified) / + gImage.framesNotified < + 0.5, + true, + "Must have also animated the background image, and essentially the same # of frames. " + + "Regular image got " + + gImage.framesNotified + + " frames but background image got " + + image.framesNotified + ); + }); + + gBrowser.removeCurrentTab(); + + nextTest(); + } + + var goer = theTest(); + goer.next(); +} + +// Check that imgContainers are shared on the same page and +// between tabs +function testSharedContainers() { + function theTest() { + var gImages = []; + var gFrames; + + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + TESTROOT + "image.html" + ); + gBrowser.selectedBrowser.addEventListener( + "pageshow", + function () { + actOnMozImage(gBrowser.contentDocument, "img1", function (image) { + gImages[0] = image; + gFrames = image.framesNotified; // May in theory have frames from last test + // in this counter - so subtract them out + }); + goer.next(); + }, + { capture: true, once: true } + ); + yield; + + // Load next tab somewhat later + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + goer.next(); + }, + 1500, + Ci.nsITimer.TYPE_ONE_SHOT + ); + yield; + + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + TESTROOT + "imageX2.html" + ); + gBrowser.selectedBrowser.addEventListener( + "pageshow", + function () { + [1, 2].forEach(function (i) { + actOnMozImage(gBrowser.contentDocument, "img" + i, function (image) { + gImages[i] = image; + }); + }); + goer.next(); + }, + { capture: true, once: true } + ); + yield; + + var chances = 120; + do { + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.initWithCallback( + function () { + if (gImages[0].framesNotified - gFrames >= 10) { + goer.send(true); + } else { + chances--; + goer.send(chances == 0); // maybe if we wait a bit, it will happen + } + }, + 500, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } while (!yield); + is( + chances > 0, + true, + "Must have been animating while showing several images" + ); + + // Check they all have the same frame counts + var theFrames = null; + [0, 1, 2].forEach(function (i) { + var frames = gImages[i].framesNotified; + if (theFrames == null) { + theFrames = frames; + } else { + is( + theFrames, + frames, + "Sharing the same imgContainer means *exactly* the same frame counts!" + ); + } + }); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + + nextTest(); + } + + var goer = theTest(); + goer.next(); +} + +var tests = [testBFCache, testSharedContainers]; + +function nextTest() { + if (!tests.length) { + finish(); + return; + } + tests.shift()(); +} + +function test() { + ignoreAllUncaughtExceptions(); + nextTest(); +} diff --git a/image/test/browser/browser_mozicon_file.js b/image/test/browser/browser_mozicon_file.js new file mode 100644 index 0000000000..8e01e0484d --- /dev/null +++ b/image/test/browser/browser_mozicon_file.js @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test_mozicon_file_no_sandbox() { + assertFileProcess(); + await createMozIconInFile("txt"); + await createMozIconInFile("exe"); + await createMozIconInFile("non-existent-bidule"); +}); diff --git a/image/test/browser/browser_mozicon_file_sandbox_headless.js b/image/test/browser/browser_mozicon_file_sandbox_headless.js new file mode 100644 index 0000000000..08e8689904 --- /dev/null +++ b/image/test/browser/browser_mozicon_file_sandbox_headless.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test_mozicon_file_with_sandbox() { + assertFileProcess(); + assertSandboxHeadless(); + await createMozIconInFile("txt"); + await createMozIconInFile("exe"); + await createMozIconInFile("non-existent-bidule"); +}); diff --git a/image/test/browser/browser_offscreen_image_in_out_of_process_iframe.js b/image/test/browser/browser_offscreen_image_in_out_of_process_iframe.js new file mode 100644 index 0000000000..71b34a4715 --- /dev/null +++ b/image/test/browser/browser_offscreen_image_in_out_of_process_iframe.js @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const parentPATH = DIRPATH + "empty.html"; +const iframePATH = DIRPATH + "empty.html"; + +const parentURL = `http://example.com/${parentPATH}`; +const iframeURL = `http://example.org/${iframePATH}`; + +add_task(async function setup_pref() { + await SpecialPowers.pushPrefEnv({ + set: [ + // To avoid throttling requestAnimationFrame callbacks in invisible + // iframes + ["layout.throttled_frame_rate", 60], + ], + }); +}); + +add_task(async function () { + const win = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + }); + + try { + const browser = win.gBrowser.selectedTab.linkedBrowser; + + BrowserTestUtils.loadURIString(browser, parentURL); + await BrowserTestUtils.browserLoaded(browser, false, parentURL); + + async function setup(url) { + const scroller = content.document.createElement("div"); + scroller.style = "width: 300px; height: 300px; overflow: scroll;"; + scroller.setAttribute("id", "scroller"); + content.document.body.appendChild(scroller); + + // Make a space bigger than display port. + const spacer = content.document.createElement("div"); + spacer.style = "width: 100%; height: 10000px;"; + scroller.appendChild(spacer); + + const iframe = content.document.createElement("iframe"); + scroller.appendChild(iframe); + + iframe.contentWindow.location = url; + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + return iframe.browsingContext; + } + + async function setupImage() { + const img = content.document.createElement("img"); + // This GIF is a 100ms interval animation. + img.setAttribute("src", "animated.gif"); + img.setAttribute("id", "img"); + content.document.body.appendChild(img); + + const spacer = content.document.createElement("div"); + spacer.style = "width: 100%; height: 10000px;"; + content.document.body.appendChild(spacer); + await new Promise(resolve => { + img.addEventListener("load", resolve, { once: true }); + }); + } + + // Returns the count of frameUpdate during |time| (in ms) period. + async function observeFrameUpdate(time) { + function ImageDecoderObserverStub() { + this.sizeAvailable = function sizeAvailable(aRequest) {}; + this.frameComplete = function frameComplete(aRequest) {}; + this.decodeComplete = function decodeComplete(aRequest) {}; + this.loadComplete = function loadComplete(aRequest) {}; + this.frameUpdate = function frameUpdate(aRequest) {}; + this.discard = function discard(aRequest) {}; + this.isAnimated = function isAnimated(aRequest) {}; + this.hasTransparency = function hasTransparency(aRequest) {}; + } + + // Start from the callback of setTimeout. + await new Promise(resolve => content.window.setTimeout(resolve, 0)); + + let frameCount = 0; + const observer = new ImageDecoderObserverStub(); + observer.frameUpdate = () => { + frameCount++; + }; + observer.loadComplete = () => { + // Ignore the frameUpdate callback along with loadComplete. It seems + // a frameUpdate sometimes happens with a loadComplete when attatching + // observer in fission world. + frameCount--; + }; + + const gObserver = SpecialPowers.Cc["@mozilla.org/image/tools;1"] + .getService(SpecialPowers.Ci.imgITools) + .createScriptedObserver(observer); + const img = content.document.getElementById("img"); + + SpecialPowers.wrap(img).addObserver(gObserver); + await new Promise(resolve => content.window.setTimeout(resolve, time)); + SpecialPowers.wrap(img).removeObserver(gObserver); + + return frameCount; + } + + // Setup an iframe which is initially scrolled out. + const iframe = await SpecialPowers.spawn(browser, [iframeURL], setup); + + // Setup a 100ms interval animated GIF image in the iframe. + await SpecialPowers.spawn(iframe, [], setupImage); + + let frameCount = await SpecialPowers.spawn( + iframe, + [1000], + observeFrameUpdate + ); + // Bug 1577084. + if (frameCount == 0) { + is(frameCount, 0, "no frameupdates"); + } else { + todo_is(frameCount, 0, "no frameupdates"); + } + + // Scroll the iframe into view. + await SpecialPowers.spawn(browser, [], async () => { + const scroller = content.document.getElementById("scroller"); + scroller.scrollTo({ left: 0, top: 9800, behavior: "smooth" }); + await new Promise(resolve => content.window.setTimeout(resolve, 1000)); + }); + + await new Promise(resolve => requestAnimationFrame(resolve)); + + frameCount = await SpecialPowers.spawn(iframe, [1000], observeFrameUpdate); + ok(frameCount > 0, "There should be frameUpdate(s)"); + + await new Promise(resolve => requestAnimationFrame(resolve)); + + await SpecialPowers.spawn(iframe, [], async () => { + const img = content.document.getElementById("img"); + // Move the image outside of the scroll port. 'position: absolute' causes + // a relow on the image element. + img.style = "position: absolute; top: 9000px;"; + await new Promise(resolve => + content.window.requestAnimationFrame(resolve) + ); + }); + + await new Promise(resolve => requestAnimationFrame(resolve)); + + frameCount = await SpecialPowers.spawn(iframe, [1000], observeFrameUpdate); + is(frameCount, 0, "No frameUpdate should happen"); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/image/test/browser/browser_sandbox_headless.ini b/image/test/browser/browser_sandbox_headless.ini new file mode 100644 index 0000000000..70cd0147ca --- /dev/null +++ b/image/test/browser/browser_sandbox_headless.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + head.js +prefs = + security.sandbox.content.headless=true +skip-if = (os != 'linux') # the pref is only used on linux + +[browser_mozicon_file_sandbox_headless.js] diff --git a/image/test/browser/empty.html b/image/test/browser/empty.html new file mode 100644 index 0000000000..a31dad3630 --- /dev/null +++ b/image/test/browser/empty.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/browser/head.js b/image/test/browser/head.js new file mode 100644 index 0000000000..29fc67a1a7 --- /dev/null +++ b/image/test/browser/head.js @@ -0,0 +1,136 @@ +const RELATIVE_DIR = "image/test/browser/"; +const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR; +const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR; + +var chrome_root = getRootDirectory(gTestPath); +const CHROMEROOT = chrome_root; + +function getImageLoading(doc, id) { + return doc.getElementById(id); +} + +// Tries to get the Moz debug image, imgIContainerDebug. Only works +// in a debug build. If we succeed, we call func(). +function actOnMozImage(doc, id, func) { + var imgContainer = getImageLoading(doc, id).getRequest( + Ci.nsIImageLoadingContent.CURRENT_REQUEST + ).image; + var mozImage; + try { + mozImage = imgContainer.QueryInterface(Ci.imgIContainerDebug); + } catch (e) { + return false; + } + func(mozImage); + return true; +} + +function assertPrefVal(name, val) { + let boolValue = Services.prefs.getBoolPref(name); + ok(boolValue === val, `pref ${name} is set to ${val}`); + if (boolValue !== val) { + throw Error(`pref ${name} is not set to ${val}`); + } +} + +function assertFileProcess() { + // Ensure that the file content process is enabled. + assertPrefVal("browser.tabs.remote.separateFileUriProcess", true); +} + +function assertSandboxHeadless() { + assertPrefVal("security.sandbox.content.headless", true); +} + +function getPage() { + let filePage = undefined; + switch (Services.appinfo.OS) { + case "WINNT": + filePage = "file:///C:/"; + break; + case "Darwin": + filePage = "file:///tmp/"; + break; + case "Linux": + filePage = "file:///tmp/"; + break; + default: + throw new Error("Unsupported operating system"); + } + return filePage; +} + +function getSize() { + let iconSize = undefined; + switch (Services.appinfo.OS) { + case "WINNT": + iconSize = 32; + break; + case "Darwin": + iconSize = 128; + break; + case "Linux": + iconSize = 128; + break; + default: + throw new Error("Unsupported operating system"); + } + return iconSize; +} + +async function createMozIconInFile(ext, expectSuccess = true) { + const kPAGE = getPage(); + const kSize = expectSuccess ? getSize() : 24; // we get 24x24 when failing, + // e.g. when remoting is + // disabled and the sandbox + // headless is enabled + + // open a tab in a file content process + let fileTab = await BrowserTestUtils.addTab(gBrowser, kPAGE, { + preferredRemoteType: "file", + }); + + // get the browser for the file content process tab + let fileBrowser = gBrowser.getBrowserForTab(fileTab); + + let checkIcon = async (_ext, _kSize, _expectSuccess) => { + const img = content.document.createElement("img"); + let waitLoad = new Promise(resolve => { + // only listen to successfull load event if we expect the image to + // actually load, e.g. with remoting disabled and sandbox headless + // enabled we dont expect it to work, and we will wait for onerror below + // to trigger. + if (_expectSuccess) { + img.addEventListener("load", resolve, { once: true }); + } + img.onerror = () => { + // With remoting enabled, + // Verified to work by forcing early `return NS_ERROR_NOT_AVAILABLE;` + // within `nsIconChannel::GetIcon(nsIURI* aURI, ByteBuf* aDataOut)` + // + // With remoting disabled and sandbox headless enabled, this should be + // the default path, since we don't add the "load" event listener. + ok(!_expectSuccess, "Error while loading moz-icon"); + resolve(); + }; + }); + img.setAttribute("src", `moz-icon://.${_ext}?size=${_kSize}`); + img.setAttribute("id", `moz-icon-${_ext}-${_kSize}`); + content.document.body.appendChild(img); + + await waitLoad; + + const icon = content.document.getElementById(`moz-icon-${_ext}-${_kSize}`); + ok(icon !== null, `got a valid ${_ext} moz-icon`); + is(icon.width, _kSize, `${_kSize} px width ${_ext} moz-icon`); + is(icon.height, _kSize, `${_kSize} px height ${_ext} moz-icon`); + }; + + await BrowserTestUtils.browserLoaded(fileBrowser); + await SpecialPowers.spawn( + fileBrowser, + [ext, kSize, expectSuccess], + checkIcon + ); + await BrowserTestUtils.removeTab(fileTab); +} diff --git a/image/test/browser/image.html b/image/test/browser/image.html new file mode 100644 index 0000000000..3831ab68a4 --- /dev/null +++ b/image/test/browser/image.html @@ -0,0 +1,23 @@ + + + + + Imagelib2 animation tests + + + + +

    Page with image

    + +
    + + diff --git a/image/test/browser/imageX2.html b/image/test/browser/imageX2.html new file mode 100644 index 0000000000..4ce953bfac --- /dev/null +++ b/image/test/browser/imageX2.html @@ -0,0 +1,14 @@ + + + + + Imagelib2 animation tests + + +

    Page with images

    + +
    + + + diff --git a/image/test/crashtests/1205923-1.html b/image/test/crashtests/1205923-1.html new file mode 100644 index 0000000000..456fc51b6e --- /dev/null +++ b/image/test/crashtests/1205923-1.html @@ -0,0 +1,36 @@ + + + + + + diff --git a/image/test/crashtests/1210745-1.gif b/image/test/crashtests/1210745-1.gif new file mode 100644 index 0000000000..92bcf72224 Binary files /dev/null and b/image/test/crashtests/1210745-1.gif differ diff --git a/image/test/crashtests/1212954-1.svg b/image/test/crashtests/1212954-1.svg new file mode 100644 index 0000000000..83dd7b9c7f --- /dev/null +++ b/image/test/crashtests/1212954-1.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/image/test/crashtests/1235605.gif b/image/test/crashtests/1235605.gif new file mode 100644 index 0000000000..e7c3ea0b87 Binary files /dev/null and b/image/test/crashtests/1235605.gif differ diff --git a/image/test/crashtests/1241728-1.html b/image/test/crashtests/1241728-1.html new file mode 100644 index 0000000000..126c02e624 --- /dev/null +++ b/image/test/crashtests/1241728-1.html @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/image/test/crashtests/1241729-1.bmp b/image/test/crashtests/1241729-1.bmp new file mode 100644 index 0000000000..e6f36d0398 Binary files /dev/null and b/image/test/crashtests/1241729-1.bmp differ diff --git a/image/test/crashtests/1241729-1.html b/image/test/crashtests/1241729-1.html new file mode 100644 index 0000000000..47f23134b4 --- /dev/null +++ b/image/test/crashtests/1241729-1.html @@ -0,0 +1,5 @@ + + + + + diff --git a/image/test/crashtests/1242093-1.html b/image/test/crashtests/1242093-1.html new file mode 100644 index 0000000000..3eab166efd --- /dev/null +++ b/image/test/crashtests/1242093-1.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/image/test/crashtests/1242778-1.png b/image/test/crashtests/1242778-1.png new file mode 100644 index 0000000000..4504d54e45 Binary files /dev/null and b/image/test/crashtests/1242778-1.png differ diff --git a/image/test/crashtests/1249576-1.png b/image/test/crashtests/1249576-1.png new file mode 100644 index 0000000000..637dafbc2b Binary files /dev/null and b/image/test/crashtests/1249576-1.png differ diff --git a/image/test/crashtests/1251091-1.html b/image/test/crashtests/1251091-1.html new file mode 100644 index 0000000000..520a393b43 --- /dev/null +++ b/image/test/crashtests/1251091-1.html @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/image/test/crashtests/1251091-1.png b/image/test/crashtests/1251091-1.png new file mode 100644 index 0000000000..078b19a569 Binary files /dev/null and b/image/test/crashtests/1251091-1.png differ diff --git a/image/test/crashtests/1253362-1.html b/image/test/crashtests/1253362-1.html new file mode 100644 index 0000000000..fdee850aab --- /dev/null +++ b/image/test/crashtests/1253362-1.html @@ -0,0 +1,11 @@ + + + + + + + +
    + + + diff --git a/image/test/crashtests/1262549-1.gif b/image/test/crashtests/1262549-1.gif new file mode 100644 index 0000000000..7cb2e769d2 Binary files /dev/null and b/image/test/crashtests/1262549-1.gif differ diff --git a/image/test/crashtests/1277397-1.jpg b/image/test/crashtests/1277397-1.jpg new file mode 100644 index 0000000000..54f21182b6 Binary files /dev/null and b/image/test/crashtests/1277397-1.jpg differ diff --git a/image/test/crashtests/1277397-2.jpg b/image/test/crashtests/1277397-2.jpg new file mode 100644 index 0000000000..f9fc8744e8 Binary files /dev/null and b/image/test/crashtests/1277397-2.jpg differ diff --git a/image/test/crashtests/1355898-1.html b/image/test/crashtests/1355898-1.html new file mode 100644 index 0000000000..2df55b34ac --- /dev/null +++ b/image/test/crashtests/1355898-1.html @@ -0,0 +1,45 @@ + + + + + + + + + Your browser doesn't appear to support the <canvas> element. + + + \ No newline at end of file diff --git a/image/test/crashtests/1375842-1.html b/image/test/crashtests/1375842-1.html new file mode 100644 index 0000000000..457966a1b9 --- /dev/null +++ b/image/test/crashtests/1375842-1.html @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/image/test/crashtests/1413762-1.gif b/image/test/crashtests/1413762-1.gif new file mode 100644 index 0000000000..5dd10b8f25 Binary files /dev/null and b/image/test/crashtests/1413762-1.gif differ diff --git a/image/test/crashtests/1443232-1.gif b/image/test/crashtests/1443232-1.gif new file mode 100644 index 0000000000..198519e7c0 Binary files /dev/null and b/image/test/crashtests/1443232-1.gif differ diff --git a/image/test/crashtests/1443232-1.html b/image/test/crashtests/1443232-1.html new file mode 100644 index 0000000000..a7302e9e16 --- /dev/null +++ b/image/test/crashtests/1443232-1.html @@ -0,0 +1,30 @@ + + + +
    + + + diff --git a/image/test/crashtests/1509998.gif b/image/test/crashtests/1509998.gif new file mode 100644 index 0000000000..05b141ba5f Binary files /dev/null and b/image/test/crashtests/1509998.gif differ diff --git a/image/test/crashtests/1526717-1.html b/image/test/crashtests/1526717-1.html new file mode 100644 index 0000000000..b120340416 --- /dev/null +++ b/image/test/crashtests/1526717-1.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/image/test/crashtests/1526717-1.png b/image/test/crashtests/1526717-1.png new file mode 100644 index 0000000000..5ac5b32744 Binary files /dev/null and b/image/test/crashtests/1526717-1.png differ diff --git a/image/test/crashtests/1629490-1-iframe.html b/image/test/crashtests/1629490-1-iframe.html new file mode 100644 index 0000000000..2d20e28eed --- /dev/null +++ b/image/test/crashtests/1629490-1-iframe.html @@ -0,0 +1 @@ + diff --git a/image/test/crashtests/1629490-1.html b/image/test/crashtests/1629490-1.html new file mode 100644 index 0000000000..f35568ca4e --- /dev/null +++ b/image/test/crashtests/1629490-1.html @@ -0,0 +1,49 @@ + + + + + +
    + +
    + + + + diff --git a/image/test/crashtests/1634839-1-iframe.html b/image/test/crashtests/1634839-1-iframe.html new file mode 100644 index 0000000000..07c1b6545e --- /dev/null +++ b/image/test/crashtests/1634839-1-iframe.html @@ -0,0 +1 @@ + diff --git a/image/test/crashtests/1634839-1.html b/image/test/crashtests/1634839-1.html new file mode 100644 index 0000000000..865f83645f --- /dev/null +++ b/image/test/crashtests/1634839-1.html @@ -0,0 +1,51 @@ + + + + + +
    + +
    + + + + diff --git a/image/test/crashtests/1634839-2-iframe.html b/image/test/crashtests/1634839-2-iframe.html new file mode 100644 index 0000000000..6907844499 --- /dev/null +++ b/image/test/crashtests/1634839-2-iframe.html @@ -0,0 +1 @@ + diff --git a/image/test/crashtests/1634839-2.html b/image/test/crashtests/1634839-2.html new file mode 100644 index 0000000000..d3629421fb --- /dev/null +++ b/image/test/crashtests/1634839-2.html @@ -0,0 +1,51 @@ + + + + + +
    + +
    + + + + diff --git a/image/test/crashtests/1676172-1-iframe.html b/image/test/crashtests/1676172-1-iframe.html new file mode 100644 index 0000000000..c989c579bb --- /dev/null +++ b/image/test/crashtests/1676172-1-iframe.html @@ -0,0 +1,13 @@ + + + + + diff --git a/image/test/crashtests/1676172-1.gif b/image/test/crashtests/1676172-1.gif new file mode 100644 index 0000000000..c5635218d5 Binary files /dev/null and b/image/test/crashtests/1676172-1.gif differ diff --git a/image/test/crashtests/1676172-1.html b/image/test/crashtests/1676172-1.html new file mode 100644 index 0000000000..08374a4772 --- /dev/null +++ b/image/test/crashtests/1676172-1.html @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/image/test/crashtests/1763581-1-iframe.html b/image/test/crashtests/1763581-1-iframe.html new file mode 100644 index 0000000000..6064183b9e --- /dev/null +++ b/image/test/crashtests/1763581-1-iframe.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/image/test/crashtests/1763581-1.gif b/image/test/crashtests/1763581-1.gif new file mode 100644 index 0000000000..237d6a1722 Binary files /dev/null and b/image/test/crashtests/1763581-1.gif differ diff --git a/image/test/crashtests/1763581-1.html b/image/test/crashtests/1763581-1.html new file mode 100644 index 0000000000..167f541a24 --- /dev/null +++ b/image/test/crashtests/1763581-1.html @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/image/test/crashtests/1763581-1.sjs b/image/test/crashtests/1763581-1.sjs new file mode 100644 index 0000000000..9fb70dd463 --- /dev/null +++ b/image/test/crashtests/1763581-1.sjs @@ -0,0 +1,71 @@ + +function getFileStream(filename) +{ + // Get the location of this sjs file, and then use that to figure out where + // to find where our other files are. + var self = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsIFile); + self.initWithPath(getState("__LOCATION__")); + var file = self.parent; + file.append(filename); + + var fileStream = Components.classes['@mozilla.org/network/file-input-stream;1'] + .createInstance(Components.interfaces.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + + return fileStream; +} + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + x = { + data, + QueryInterface: ChromeUtils.generateQI([]), + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +const DELAY_MS = 100; +let gTimer; + +function handleRequest(request, response) { + let count = getGlobalState("count"); + if (count == null) { + count = 0; + } + + if (count > 0) { + response.processAsync(); + + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.init( + () => { + response.setStatusLine(request.httpVersion, 304, "Not Modified"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + return; + } + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/gif", false); + + var inputStream = getFileStream("1763581-1.gif"); + response.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + + count++; + setGlobalState(count, "count"); +} diff --git a/image/test/crashtests/1765871-1.jpg b/image/test/crashtests/1765871-1.jpg new file mode 100644 index 0000000000..11460a62c3 Binary files /dev/null and b/image/test/crashtests/1765871-1.jpg differ diff --git a/image/test/crashtests/1814553.avif b/image/test/crashtests/1814553.avif new file mode 100644 index 0000000000..7e2f040c53 Binary files /dev/null and b/image/test/crashtests/1814553.avif differ diff --git a/image/test/crashtests/1814561.avif b/image/test/crashtests/1814561.avif new file mode 100644 index 0000000000..33ad44d2a3 Binary files /dev/null and b/image/test/crashtests/1814561.avif differ diff --git a/image/test/crashtests/1814677.avif b/image/test/crashtests/1814677.avif new file mode 100644 index 0000000000..b91d4f955b Binary files /dev/null and b/image/test/crashtests/1814677.avif differ diff --git a/image/test/crashtests/1814708.avif b/image/test/crashtests/1814708.avif new file mode 100644 index 0000000000..21a39cfa4d Binary files /dev/null and b/image/test/crashtests/1814708.avif differ diff --git a/image/test/crashtests/1814741.avif b/image/test/crashtests/1814741.avif new file mode 100644 index 0000000000..842eaea1c1 Binary files /dev/null and b/image/test/crashtests/1814741.avif differ diff --git a/image/test/crashtests/1814774.avif b/image/test/crashtests/1814774.avif new file mode 100644 index 0000000000..77ffba7af2 Binary files /dev/null and b/image/test/crashtests/1814774.avif differ diff --git a/image/test/crashtests/1817108.avif b/image/test/crashtests/1817108.avif new file mode 100644 index 0000000000..a06b923ee4 Binary files /dev/null and b/image/test/crashtests/1817108.avif differ diff --git a/image/test/crashtests/256-height.ico b/image/test/crashtests/256-height.ico new file mode 100644 index 0000000000..6a3c5c1944 Binary files /dev/null and b/image/test/crashtests/256-height.ico differ diff --git a/image/test/crashtests/256-width.ico b/image/test/crashtests/256-width.ico new file mode 100644 index 0000000000..a82983ce4f Binary files /dev/null and b/image/test/crashtests/256-width.ico differ diff --git a/image/test/crashtests/463696.bmp b/image/test/crashtests/463696.bmp new file mode 100644 index 0000000000..ec80d54126 Binary files /dev/null and b/image/test/crashtests/463696.bmp differ diff --git a/image/test/crashtests/523528-1.gif b/image/test/crashtests/523528-1.gif new file mode 100644 index 0000000000..abadca7ad2 Binary files /dev/null and b/image/test/crashtests/523528-1.gif differ diff --git a/image/test/crashtests/523528-2.gif b/image/test/crashtests/523528-2.gif new file mode 100644 index 0000000000..5be3bd46f6 Binary files /dev/null and b/image/test/crashtests/523528-2.gif differ diff --git a/image/test/crashtests/570451.png b/image/test/crashtests/570451.png new file mode 100644 index 0000000000..c49f2d11f1 Binary files /dev/null and b/image/test/crashtests/570451.png differ diff --git a/image/test/crashtests/694165-1.xhtml b/image/test/crashtests/694165-1.xhtml new file mode 100644 index 0000000000..1e340a0f2f --- /dev/null +++ b/image/test/crashtests/694165-1.xhtml @@ -0,0 +1,510 @@ + + +]> + + + + diff --git a/image/test/crashtests/732319-1.html b/image/test/crashtests/732319-1.html new file mode 100644 index 0000000000..b9d9c6de87 --- /dev/null +++ b/image/test/crashtests/732319-1.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/crashtests/83804-1.gif b/image/test/crashtests/83804-1.gif new file mode 100644 index 0000000000..3967c703f6 Binary files /dev/null and b/image/test/crashtests/83804-1.gif differ diff --git a/image/test/crashtests/844403-1.html b/image/test/crashtests/844403-1.html new file mode 100644 index 0000000000..ef207c0f9f --- /dev/null +++ b/image/test/crashtests/844403-1.html @@ -0,0 +1,10 @@ + + + diff --git a/image/test/crashtests/856616.gif b/image/test/crashtests/856616.gif new file mode 100644 index 0000000000..0fac811014 Binary files /dev/null and b/image/test/crashtests/856616.gif differ diff --git a/image/test/crashtests/89341-1.gif b/image/test/crashtests/89341-1.gif new file mode 100644 index 0000000000..14b3892d17 Binary files /dev/null and b/image/test/crashtests/89341-1.gif differ diff --git a/image/test/crashtests/944353.jpg b/image/test/crashtests/944353.jpg new file mode 100644 index 0000000000..fd81c58263 Binary files /dev/null and b/image/test/crashtests/944353.jpg differ diff --git a/image/test/crashtests/colormap-range.gif b/image/test/crashtests/colormap-range.gif new file mode 100644 index 0000000000..887add653f Binary files /dev/null and b/image/test/crashtests/colormap-range.gif differ diff --git a/image/test/crashtests/crashtests.list b/image/test/crashtests/crashtests.list new file mode 100644 index 0000000000..055ad677a1 --- /dev/null +++ b/image/test/crashtests/crashtests.list @@ -0,0 +1,84 @@ +# Bug 668068 - Maximum (256) width and height icons that we currently interpret as 0-width and 0-height. +load 256-height.ico +load 256-width.ico + +load 83804-1.gif +load 89341-1.gif +load 463696.bmp +load 570451.png +# Bug 1390704 - Skip on debug because it triggers a quadratic behavior that makes it take +# so much time that it can trip on the reftest timeout of 5 minutes. +skip-if(Android||isDebugBuild||ThreadSanitizer) load 694165-1.xhtml +load 732319-1.html +load 844403-1.html +load 856616.gif +skip-if(ThreadSanitizer) load 944353.jpg +load 1205923-1.html +# Ensure we handle detecting that an image is animated, then failing to decode +# it. (See bug 1210745.) +load 1210745-1.gif +load 1212954-1.svg +load 1235605.gif +load 1241728-1.html +load 1241729-1.html +load 1242093-1.html +skip-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)) load 1242778-1.png +load 1249576-1.png +load 1253362-1.html +skip-if(Android&&browserIsRemote) load 1355898-1.html # bug 1507207 +load 1375842-1.html +load 1413762-1.gif +pref(image.downscale-during-decode.enabled,true) load 1443232-1.html +load colormap-range.gif +HTTP load delayedframe.sjs # A 3-frame animated GIF with an inordinate delay between the second and third frame + +# Animated gifs with a very large canvas, but tiny actual content. +load delaytest.html?523528-1.gif +load delaytest.html?523528-2.gif + +load delaytest.html?1262549-1.gif +load delaytest.html?1277397-1.jpg +load delaytest.html?1277397-2.jpg + +# Bug 1160801 - Ensure that we handle invalid disposal types. +load invalid-disposal-method-1.gif +load invalid-disposal-method-2.gif +load invalid-disposal-method-3.gif + +load invalid-icc-profile.jpg # This would have exposed the leak discovered in bug 642902 + +# Ensure we handle ICO directory entries which specify the wrong size for the contained resource. +load invalid_ico_height.ico +load invalid_ico_width.ico + +# Bug 525326 - Test image sizes of 65535x65535 which is larger than we allow) +load invalid-size.gif +load invalid-size-second-frame.gif + +load multiple-png-hassize.ico # Bug 863958 - This icon's size is such that it leads to multiple writes to the PNG decoder after we've gotten our size. +skip-if(ThreadSanitizer) asserts(0-2) load ownerdiscard.html # Bug 1323672, bug 807211 +load truncated-second-frame.png # Bug 863975 + +# Bug 1509998 - Ensure that we handle empty frame rects in animated images. +load 1509998.gif + +load 1526717-1.html +skip-if(ThreadSanitizer) pref(widget.windows.window_occlusion_tracking.enabled,false) load 1629490-1.html # Bug 1819154 + +pref(widget.windows.window_occlusion_tracking.enabled,false) HTTP load 1634839-1.html # Bug 1819154 +pref(widget.windows.window_occlusion_tracking.enabled,false) HTTP load 1634839-2.html # Bug 1819154 +pref(image.animated.decode-on-demand.batch-size,1) pref(image.animated.decode-on-demand.threshold-kb,0) HTTP load 1676172-1.html +pref(browser.soft_reload.only_force_validate_top_level_document,false) HTTP load 1763581-1.html +load 1765871-1.jpg + +load 1814561.avif +load 1814708.avif + +load 1814553.avif + +load 1814677.avif + +load 1814741.avif + +load 1814774.avif +load 1817108.avif diff --git a/image/test/crashtests/delayedframe.sjs b/image/test/crashtests/delayedframe.sjs new file mode 100644 index 0000000000..0cd7ce97e9 --- /dev/null +++ b/image/test/crashtests/delayedframe.sjs @@ -0,0 +1,44 @@ +function getFileStream(filename) +{ + // Get the location of this sjs file, and then use that to figure out where + // to find where our other files are. + var self = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsIFile); + self.initWithPath(getState("__LOCATION__")); + var file = self.parent; + file.append(filename); + dump(file.path + "\n"); + + var fileStream = Components.classes['@mozilla.org/network/file-input-stream;1'] + .createInstance(Components.interfaces.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + + return fileStream; +} + +var gTimer; + +function handleRequest(request, response) +{ + response.processAsync(); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/gif", false); + + var firststream = getFileStream("threeframes-start.gif"); + response.bodyOutputStream.writeFrom(firststream, firststream.available()) + firststream.close(); + + gTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + gTimer.initWithCallback(function() + { + var secondstream = getFileStream("threeframes-end.gif"); + response.bodyOutputStream.writeFrom(secondstream, secondstream.available()) + secondstream.close(); + response.finish(); + + // This time needs to be longer than the animation timer in + // threeframes-start.gif. That's specified as 100ms; just use 5 seconds as + // a reasonable upper bound. Since this is just a crashtest, timeouts + // aren't a big deal. + }, 5 * 1000 /* milliseconds */, Components.interfaces.nsITimer.TYPE_ONE_SHOT); +} diff --git a/image/test/crashtests/delaytest.html b/image/test/crashtests/delaytest.html new file mode 100644 index 0000000000..fb368c1e90 --- /dev/null +++ b/image/test/crashtests/delaytest.html @@ -0,0 +1,59 @@ + + + +Delayed image reftest wrapper + + + + + + + diff --git a/image/test/crashtests/discardframe.htm b/image/test/crashtests/discardframe.htm new file mode 100644 index 0000000000..5ced0029c5 --- /dev/null +++ b/image/test/crashtests/discardframe.htm @@ -0,0 +1 @@ + diff --git a/image/test/crashtests/finite-apng.png b/image/test/crashtests/finite-apng.png new file mode 100644 index 0000000000..778613d851 Binary files /dev/null and b/image/test/crashtests/finite-apng.png differ diff --git a/image/test/crashtests/ie.png b/image/test/crashtests/ie.png new file mode 100644 index 0000000000..74c4a1a323 Binary files /dev/null and b/image/test/crashtests/ie.png differ diff --git a/image/test/crashtests/invalid-disposal-method-1.gif b/image/test/crashtests/invalid-disposal-method-1.gif new file mode 100644 index 0000000000..30c61de188 Binary files /dev/null and b/image/test/crashtests/invalid-disposal-method-1.gif differ diff --git a/image/test/crashtests/invalid-disposal-method-2.gif b/image/test/crashtests/invalid-disposal-method-2.gif new file mode 100644 index 0000000000..66158d81a9 Binary files /dev/null and b/image/test/crashtests/invalid-disposal-method-2.gif differ diff --git a/image/test/crashtests/invalid-disposal-method-3.gif b/image/test/crashtests/invalid-disposal-method-3.gif new file mode 100644 index 0000000000..0da0723773 Binary files /dev/null and b/image/test/crashtests/invalid-disposal-method-3.gif differ diff --git a/image/test/crashtests/invalid-icc-profile.jpg b/image/test/crashtests/invalid-icc-profile.jpg new file mode 100644 index 0000000000..938c7713ce Binary files /dev/null and b/image/test/crashtests/invalid-icc-profile.jpg differ diff --git a/image/test/crashtests/invalid-size-second-frame.gif b/image/test/crashtests/invalid-size-second-frame.gif new file mode 100644 index 0000000000..22005ae4ca Binary files /dev/null and b/image/test/crashtests/invalid-size-second-frame.gif differ diff --git a/image/test/crashtests/invalid-size.gif b/image/test/crashtests/invalid-size.gif new file mode 100644 index 0000000000..665ca9b5dc Binary files /dev/null and b/image/test/crashtests/invalid-size.gif differ diff --git a/image/test/crashtests/invalid_ico_height.ico b/image/test/crashtests/invalid_ico_height.ico new file mode 100644 index 0000000000..50d6842278 Binary files /dev/null and b/image/test/crashtests/invalid_ico_height.ico differ diff --git a/image/test/crashtests/invalid_ico_width.ico b/image/test/crashtests/invalid_ico_width.ico new file mode 100644 index 0000000000..4ace07c16f Binary files /dev/null and b/image/test/crashtests/invalid_ico_width.ico differ diff --git a/image/test/crashtests/multiple-png-hassize.ico b/image/test/crashtests/multiple-png-hassize.ico new file mode 100644 index 0000000000..6944220018 Binary files /dev/null and b/image/test/crashtests/multiple-png-hassize.ico differ diff --git a/image/test/crashtests/out2.gif b/image/test/crashtests/out2.gif new file mode 100644 index 0000000000..f062c7f20c Binary files /dev/null and b/image/test/crashtests/out2.gif differ diff --git a/image/test/crashtests/ownerdiscard.html b/image/test/crashtests/ownerdiscard.html new file mode 100644 index 0000000000..2e5be86023 --- /dev/null +++ b/image/test/crashtests/ownerdiscard.html @@ -0,0 +1,48 @@ + + + +
    + + +
    + + + diff --git a/image/test/crashtests/rainbow.gif b/image/test/crashtests/rainbow.gif new file mode 100644 index 0000000000..72a7816928 Binary files /dev/null and b/image/test/crashtests/rainbow.gif differ diff --git a/image/test/crashtests/threeframes-end.gif b/image/test/crashtests/threeframes-end.gif new file mode 100644 index 0000000000..baf6a418c2 Binary files /dev/null and b/image/test/crashtests/threeframes-end.gif differ diff --git a/image/test/crashtests/threeframes-start.gif b/image/test/crashtests/threeframes-start.gif new file mode 100644 index 0000000000..bc641a3166 Binary files /dev/null and b/image/test/crashtests/threeframes-start.gif differ diff --git a/image/test/crashtests/truncated-second-frame.png b/image/test/crashtests/truncated-second-frame.png new file mode 100644 index 0000000000..0aef5e44de Binary files /dev/null and b/image/test/crashtests/truncated-second-frame.png differ diff --git a/image/test/crashtests/unsized-svg.svg b/image/test/crashtests/unsized-svg.svg new file mode 100644 index 0000000000..714efc7ef0 --- /dev/null +++ b/image/test/crashtests/unsized-svg.svg @@ -0,0 +1 @@ + diff --git a/image/test/fuzzing/TestDecoders.cpp b/image/test/fuzzing/TestDecoders.cpp new file mode 100644 index 0000000000..8515ff43c6 --- /dev/null +++ b/image/test/fuzzing/TestDecoders.cpp @@ -0,0 +1,180 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "imgIContainer.h" +#include "imgITools.h" +#include "ImageOps.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/Preferences.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "mozilla/RefPtr.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +#include "FuzzingInterfaceStream.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +// Prevents x being optimized away if it has no side-effects. +// If optimized away, tools like ASan wouldn't be able to detect +// faulty memory accesses. +#define DUMMY_IF(x) \ + if (x) { \ + volatile int v; \ + v = 0; \ + (void)v; \ + } + +class DecodeToSurfaceRunnableFuzzing : public Runnable { + public: + DecodeToSurfaceRunnableFuzzing(RefPtr& aSurface, + nsIInputStream* aInputStream, + const char* mimeType) + : mozilla::Runnable("DecodeToSurfaceRunnableFuzzing"), + mSurface(aSurface), + mInputStream(aInputStream), + mMimeType(mimeType) {} + + NS_IMETHOD Run() override { + Go(); + return NS_OK; + } + + void Go() { + mSurface = ImageOps::DecodeToSurface(mInputStream.forget(), mMimeType, + imgIContainer::DECODE_FLAGS_DEFAULT); + if (!mSurface) return; + + if (mSurface->GetType() == SurfaceType::DATA) { + if (mSurface->GetFormat() == SurfaceFormat::OS_RGBX || + mSurface->GetFormat() == SurfaceFormat::OS_RGBA) { + DUMMY_IF(IntSize(1, 1) == mSurface->GetSize()); + DUMMY_IF(IsSolidColor(mSurface, BGRAColor::Green(), 1)); + } + } + } + + private: + RefPtr& mSurface; + nsCOMPtr mInputStream; + nsAutoCString mMimeType; +}; + +static int RunDecodeToSurfaceFuzzing(nsCOMPtr inputStream, + const char* mimeType) { + uint64_t len; + inputStream->Available(&len); + if (len <= 0) { + return 0; + } + + // Ensure CMS state is initialized on the main thread. + gfxPlatform::GetCMSMode(); + + nsCOMPtr thread; + nsresult rv = + NS_NewNamedThread("Decoder Test", getter_AddRefs(thread), nullptr); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + + // We run the DecodeToSurface tests off-main-thread to ensure that + // DecodeToSurface doesn't require any other main-thread-only code. + RefPtr surface; + nsCOMPtr runnable = + new DecodeToSurfaceRunnableFuzzing(surface, inputStream, mimeType); + NS_DispatchAndSpinEventLoopUntilComplete("RunDecodeToSurfaceFuzzing"_ns, + thread, runnable.forget()); + + thread->Shutdown(); + + // Explicitly release the SourceSurface on the main thread. + surface = nullptr; + + return 0; +} + +static int RunDecodeToSurfaceFuzzingJPEG(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/jpeg"); +} + +static int RunDecodeToSurfaceFuzzingGIF(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/gif"); +} + +static int RunDecodeToSurfaceFuzzingICO(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/ico"); +} + +static int RunDecodeToSurfaceFuzzingBMP(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/bmp"); +} + +static int RunDecodeToSurfaceFuzzingPNG(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/png"); +} + +static int RunDecodeToSurfaceFuzzingWebP(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/webp"); +} + +static int RunDecodeToSurfaceFuzzingAVIF(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/avif"); +} + +#ifdef MOZ_JXL +static int RunDecodeToSurfaceFuzzingJXL(nsCOMPtr inputStream) { + return RunDecodeToSurfaceFuzzing(inputStream, "image/jxl"); +} +#endif + +int FuzzingInitImage(int* argc, char*** argv) { + Preferences::SetBool("image.avif.sequence.enabled", true); +#ifdef MOZ_JXL + Preferences::SetBool("image.jxl.enabled", true); +#endif + + nsCOMPtr imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + if (imgTools == nullptr) { + std::cerr << "Initializing image tools failed" << std::endl; + return 1; + } + + return 0; +} + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingJPEG, + ImageJPEG); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingGIF, + ImageGIF); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingICO, + ImageICO); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingBMP, + ImageBMP); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingPNG, + ImagePNG); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingWebP, + ImageWebP); + +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingAVIF, + ImageAVIF); + +#ifdef MOZ_JXL +MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitImage, RunDecodeToSurfaceFuzzingJXL, + ImageJXL); +#endif diff --git a/image/test/fuzzing/moz.build b/image/test/fuzzing/moz.build new file mode 100644 index 0000000000..24af56396f --- /dev/null +++ b/image/test/fuzzing/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Library("FuzzingImage") + +SOURCES += [ + "TestDecoders.cpp", +] + +FINAL_LIBRARY = "xul-gtest" + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", + "/gfx/2d", + "/image", + "/image/test/gtest", +] + +LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"] diff --git a/image/test/gtest/Common.cpp b/image/test/gtest/Common.cpp new file mode 100644 index 0000000000..d0ddbecedf --- /dev/null +++ b/image/test/gtest/Common.cpp @@ -0,0 +1,1076 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Common.h" + +#include + +#include "gfxPlatform.h" + +#include "ImageFactory.h" +#include "imgITools.h" +#include "mozilla/Preferences.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIProperties.h" +#include "nsNetUtil.h" +#include "mozilla/RefPtr.h" +#include "nsStreamUtils.h" +#include "nsString.h" + +namespace mozilla { +namespace image { + +using namespace gfx; + +using std::vector; + +static bool sImageLibInitialized = false; + +AutoInitializeImageLib::AutoInitializeImageLib() { + if (MOZ_LIKELY(sImageLibInitialized)) { + return; + } + + EXPECT_TRUE(NS_IsMainThread()); + sImageLibInitialized = true; + + // Ensure WebP is enabled to run decoder tests. + nsresult rv = Preferences::SetBool("image.webp.enabled", true); + EXPECT_TRUE(rv == NS_OK); + + // Ensure AVIF is enabled to run decoder tests. + rv = Preferences::SetBool("image.avif.enabled", true); + EXPECT_TRUE(rv == NS_OK); + rv = Preferences::SetBool("image.avif.sequence.enabled", true); + EXPECT_TRUE(rv == NS_OK); + +#ifdef MOZ_JXL + // Ensure JXL is enabled to run decoder tests. + rv = Preferences::SetBool("image.jxl.enabled", true); + EXPECT_TRUE(rv == NS_OK); +#endif + + // Ensure that ImageLib services are initialized. + nsCOMPtr imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + EXPECT_TRUE(imgTools != nullptr); + + // Ensure gfxPlatform is initialized. + gfxPlatform::GetPlatform(); + + // Ensure we always color manage images with gtests. + gfxPlatform::SetCMSModeOverride(CMSMode::All); + + // Depending on initialization order, it is possible that our pref changes + // have not taken effect yet because there are pending gfx-related events on + // the main thread. + SpinPendingEvents(); +} + +void ImageBenchmarkBase::SetUp() { + nsCOMPtr inputStream = LoadFile(mTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into a SourceBuffer. + mSourceBuffer = new SourceBuffer(); + mSourceBuffer->ExpectLength(length); + rv = mSourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + mSourceBuffer->Complete(NS_OK); +} + +void ImageBenchmarkBase::TearDown() {} + +/////////////////////////////////////////////////////////////////////////////// +// General Helpers +/////////////////////////////////////////////////////////////////////////////// + +// These macros work like gtest's ASSERT_* macros, except that they can be used +// in functions that return values. +#define ASSERT_TRUE_OR_RETURN(e, rv) \ + EXPECT_TRUE(e); \ + if (!(e)) { \ + return rv; \ + } + +#define ASSERT_EQ_OR_RETURN(a, b, rv) \ + EXPECT_EQ(a, b); \ + if ((a) != (b)) { \ + return rv; \ + } + +#define ASSERT_GE_OR_RETURN(a, b, rv) \ + EXPECT_GE(a, b); \ + if (!((a) >= (b))) { \ + return rv; \ + } + +#define ASSERT_LE_OR_RETURN(a, b, rv) \ + EXPECT_LE(a, b); \ + if (!((a) <= (b))) { \ + return rv; \ + } + +#define ASSERT_LT_OR_RETURN(a, b, rv) \ + EXPECT_LT(a, b); \ + if (!((a) < (b))) { \ + return rv; \ + } + +void SpinPendingEvents() { + nsCOMPtr mainThread = do_GetMainThread(); + EXPECT_TRUE(mainThread != nullptr); + + bool processed; + do { + processed = false; + nsresult rv = mainThread->ProcessNextEvent(false, &processed); + EXPECT_NS_SUCCEEDED(rv); + } while (processed); +} + +already_AddRefed LoadFile(const char* aRelativePath) { + nsresult rv; + + nsCOMPtr dirService = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); + ASSERT_TRUE_OR_RETURN(dirService != nullptr, nullptr); + + // Retrieve the current working directory. + nsCOMPtr file; + rv = dirService->Get(NS_OS_CURRENT_WORKING_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(file)); + ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); + // Construct the final path by appending the working path to the current + // working directory. + file->AppendNative(nsDependentCString(aRelativePath)); + + // Construct an input stream for the requested file. + nsCOMPtr inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), file); + ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); + + // Ensure the resulting input stream is buffered. + if (!NS_InputStreamIsBuffered(inputStream)) { + nsCOMPtr bufStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream), + inputStream.forget(), 1024); + ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); + inputStream = bufStream; + } + + return inputStream.forget(); +} + +bool IsSolidColor(SourceSurface* aSurface, BGRAColor aColor, + uint8_t aFuzz /* = 0 */) { + IntSize size = aSurface->GetSize(); + return RectIsSolidColor(aSurface, IntRect(0, 0, size.width, size.height), + aColor, aFuzz); +} + +bool RowsAreSolidColor(SourceSurface* aSurface, int32_t aStartRow, + int32_t aRowCount, BGRAColor aColor, + uint8_t aFuzz /* = 0 */) { + IntSize size = aSurface->GetSize(); + return RectIsSolidColor( + aSurface, IntRect(0, aStartRow, size.width, aRowCount), aColor, aFuzz); +} + +bool RectIsSolidColor(SourceSurface* aSurface, const IntRect& aRect, + BGRAColor aColor, uint8_t aFuzz /* = 0 */) { + IntSize surfaceSize = aSurface->GetSize(); + IntRect rect = + aRect.Intersect(IntRect(0, 0, surfaceSize.width, surfaceSize.height)); + + RefPtr dataSurface = aSurface->GetDataSurface(); + ASSERT_TRUE_OR_RETURN(dataSurface != nullptr, false); + + DataSourceSurface::ScopedMap mapping(dataSurface, + DataSourceSurface::MapType::READ); + ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false); + ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false); + + uint8_t* data = mapping.GetData(); + ASSERT_TRUE_OR_RETURN(data != nullptr, false); + + BGRAColor pmColor = aColor.Premultiply(); + uint32_t expectedPixel = pmColor.AsPixel(); + + int32_t rowLength = mapping.GetStride(); + for (int32_t row = rect.Y(); row < rect.YMost(); ++row) { + for (int32_t col = rect.X(); col < rect.XMost(); ++col) { + int32_t i = row * rowLength + col * 4; + uint32_t gotPixel = *reinterpret_cast(data + i); + if (expectedPixel != gotPixel) { + BGRAColor gotColor = BGRAColor::FromPixel(gotPixel); + if (abs(pmColor.mBlue - gotColor.mBlue) > aFuzz || + abs(pmColor.mGreen - gotColor.mGreen) > aFuzz || + abs(pmColor.mRed - gotColor.mRed) > aFuzz || + abs(pmColor.mAlpha - gotColor.mAlpha) > aFuzz) { + EXPECT_EQ(expectedPixel, gotPixel) + << "Color mismatch for rectangle from " << aRect.TopLeft() + << " to " << aRect.BottomRight() << ": " + << "got rgba(" << static_cast(gotColor.mRed) << ", " + << static_cast(gotColor.mGreen) << ", " + << static_cast(gotColor.mBlue) << ", " + << static_cast(gotColor.mAlpha) << "), " + << "expected rgba(" << static_cast(pmColor.mRed) << ", " + << static_cast(pmColor.mGreen) << ", " + << static_cast(pmColor.mBlue) << ", " + << static_cast(pmColor.mAlpha) << ")"; + return false; + } + } + } + } + + return true; +} + +bool RowHasPixels(SourceSurface* aSurface, int32_t aRow, + const vector& aPixels) { + ASSERT_GE_OR_RETURN(aRow, 0, false); + + IntSize surfaceSize = aSurface->GetSize(); + ASSERT_EQ_OR_RETURN(aPixels.size(), size_t(surfaceSize.width), false); + ASSERT_LT_OR_RETURN(aRow, surfaceSize.height, false); + + RefPtr dataSurface = aSurface->GetDataSurface(); + ASSERT_TRUE_OR_RETURN(dataSurface, false); + + DataSourceSurface::ScopedMap mapping(dataSurface, + DataSourceSurface::MapType::READ); + ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false); + ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false); + + uint8_t* data = mapping.GetData(); + ASSERT_TRUE_OR_RETURN(data != nullptr, false); + + int32_t rowLength = mapping.GetStride(); + for (int32_t col = 0; col < surfaceSize.width; ++col) { + int32_t i = aRow * rowLength + col * 4; + uint32_t gotPixelData = *reinterpret_cast(data + i); + BGRAColor gotPixel = BGRAColor::FromPixel(gotPixelData); + EXPECT_EQ(aPixels[col].mBlue, gotPixel.mBlue); + EXPECT_EQ(aPixels[col].mGreen, gotPixel.mGreen); + EXPECT_EQ(aPixels[col].mRed, gotPixel.mRed); + EXPECT_EQ(aPixels[col].mAlpha, gotPixel.mAlpha); + ASSERT_EQ_OR_RETURN(aPixels[col].AsPixel(), gotPixelData, false); + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// SurfacePipe Helpers +/////////////////////////////////////////////////////////////////////////////// + +already_AddRefed CreateTrivialDecoder() { + DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif"); + auto sourceBuffer = MakeNotNull>(); + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, Nothing(), DefaultDecoderFlags(), + DefaultSurfaceFlags()); + return decoder.forget(); +} + +void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter, + const IntRect& aInputSpaceRect, + const IntRect& aOutputSpaceRect) { + EXPECT_TRUE(aFilter->IsSurfaceFinished()); + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(aInputSpaceRect, invalidRect->mInputSpaceRect.ToUnknownRect()); + EXPECT_EQ(aOutputSpaceRect, invalidRect->mOutputSpaceRect.ToUnknownRect()); +} + +void CheckGeneratedImage(Decoder* aDecoder, const IntRect& aRect, + uint8_t aFuzz /* = 0 */) { + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + CheckGeneratedSurface(surface, aRect, BGRAColor::Green(), + BGRAColor::Transparent(), aFuzz); +} + +void CheckGeneratedSurface(SourceSurface* aSurface, const IntRect& aRect, + const BGRAColor& aInnerColor, + const BGRAColor& aOuterColor, + uint8_t aFuzz /* = 0 */) { + const IntSize surfaceSize = aSurface->GetSize(); + + // This diagram shows how the surface is divided into regions that the code + // below tests for the correct content. The output rect is the bounds of the + // region labeled 'C'. + // + // +---------------------------+ + // | A | + // +---------+--------+--------+ + // | B | C | D | + // +---------+--------+--------+ + // | E | + // +---------------------------+ + + // Check that the output rect itself is the inner color. (Region 'C'.) + EXPECT_TRUE(RectIsSolidColor(aSurface, aRect, aInnerColor, aFuzz)); + + // Check that the area above the output rect is the outer color. (Region 'A'.) + EXPECT_TRUE(RectIsSolidColor(aSurface, + IntRect(0, 0, surfaceSize.width, aRect.Y()), + aOuterColor, aFuzz)); + + // Check that the area to the left of the output rect is the outer color. + // (Region 'B'.) + EXPECT_TRUE(RectIsSolidColor(aSurface, + IntRect(0, aRect.Y(), aRect.X(), aRect.YMost()), + aOuterColor, aFuzz)); + + // Check that the area to the right of the output rect is the outer color. + // (Region 'D'.) + const int32_t widthOnRight = surfaceSize.width - aRect.XMost(); + EXPECT_TRUE(RectIsSolidColor( + aSurface, IntRect(aRect.XMost(), aRect.Y(), widthOnRight, aRect.YMost()), + aOuterColor, aFuzz)); + + // Check that the area below the output rect is the outer color. (Region 'E'.) + const int32_t heightBelow = surfaceSize.height - aRect.YMost(); + EXPECT_TRUE(RectIsSolidColor( + aSurface, IntRect(0, aRect.YMost(), surfaceSize.width, heightBelow), + aOuterColor, aFuzz)); +} + +void CheckWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter, + const Maybe& aOutputRect /* = Nothing() */, + const Maybe& aInputRect /* = Nothing() */, + const Maybe& aInputWriteRect /* = Nothing() */, + const Maybe& aOutputWriteRect /* = Nothing() */, + uint8_t aFuzz /* = 0 */) { + CheckTransformedWritePixels(aDecoder, aFilter, BGRAColor::Green(), + BGRAColor::Green(), aOutputRect, aInputRect, + aInputWriteRect, aOutputWriteRect, aFuzz); +} + +void CheckTransformedWritePixels( + Decoder* aDecoder, SurfaceFilter* aFilter, const BGRAColor& aInputColor, + const BGRAColor& aOutputColor, + const Maybe& aOutputRect /* = Nothing() */, + const Maybe& aInputRect /* = Nothing() */, + const Maybe& aInputWriteRect /* = Nothing() */, + const Maybe& aOutputWriteRect /* = Nothing() */, + uint8_t aFuzz /* = 0 */) { + IntRect outputRect = aOutputRect.valueOr(IntRect(0, 0, 100, 100)); + IntRect inputRect = aInputRect.valueOr(IntRect(0, 0, 100, 100)); + IntRect inputWriteRect = aInputWriteRect.valueOr(inputRect); + IntRect outputWriteRect = aOutputWriteRect.valueOr(outputRect); + + // Fill the image. + int32_t count = 0; + auto result = aFilter->WritePixels([&] { + ++count; + return AsVariant(aInputColor.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(inputWriteRect.Width() * inputWriteRect.Height(), count); + + AssertCorrectPipelineFinalState(aFilter, inputRect, outputRect); + + // Attempt to write more data and make sure nothing changes. + const int32_t oldCount = count; + result = aFilter->WritePixels([&] { + ++count; + return AsVariant(aInputColor.AsPixel()); + }); + EXPECT_EQ(oldCount, count); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_TRUE(aFilter->IsSurfaceFinished()); + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Attempt to advance to the next row and make sure nothing changes. + aFilter->AdvanceRow(); + EXPECT_TRUE(aFilter->IsSurfaceFinished()); + invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + CheckGeneratedSurface(surface, outputWriteRect, aOutputColor, + BGRAColor::Transparent(), aFuzz); +} + +/////////////////////////////////////////////////////////////////////////////// +// Test Data +/////////////////////////////////////////////////////////////////////////////// + +ImageTestCase GreenPNGTestCase() { + return ImageTestCase("green.png", "image/png", IntSize(100, 100)); +} + +ImageTestCase GreenGIFTestCase() { + return ImageTestCase("green.gif", "image/gif", IntSize(100, 100)); +} + +ImageTestCase GreenJPGTestCase() { + return ImageTestCase("green.jpg", "image/jpeg", IntSize(100, 100), + TEST_CASE_IS_FUZZY); +} + +ImageTestCase GreenBMPTestCase() { + return ImageTestCase("green.bmp", "image/bmp", IntSize(100, 100)); +} + +ImageTestCase GreenICOTestCase() { + // This ICO contains a 32-bit BMP, and we use a BMP's alpha data by default + // when the BMP is embedded in an ICO, so it's transparent. + return ImageTestCase("green.ico", "image/x-icon", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase GreenIconTestCase() { + return ImageTestCase("green.icon", "image/icon", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase GreenWebPTestCase() { + return ImageTestCase("green.webp", "image/webp", IntSize(100, 100)); +} + +ImageTestCase GreenAVIFTestCase() { + return ImageTestCase("green.avif", "image/avif", IntSize(100, 100)); +} + +ImageTestCase NonzeroReservedAVIFTestCase() { + auto testCase = ImageTestCase("hdlr-nonzero-reserved-bug-1727033.avif", + "image/avif", IntSize(1, 1)); + testCase.mColor = BGRAColor(0x00, 0x00, 0x00, 0xFF); + return testCase; +} + +ImageTestCase MultipleColrAVIFTestCase() { + auto testCase = ImageTestCase("valid-avif-colr-nclx-and-prof.avif", + "image/avif", IntSize(1, 1)); + testCase.mColor = BGRAColor(0x00, 0x00, 0x00, 0xFF); + return testCase; +} + +ImageTestCase Transparent10bit420AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-10bit-yuv420.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent10bit422AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-10bit-yuv422.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent10bit444AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-10bit-yuv444.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent12bit420AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-12bit-yuv420.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent12bit422AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-12bit-yuv422.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent12bit444AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-12bit-yuv444.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + testCase.mColor = BGRAColor(0x00, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent8bit420AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-8bit-yuv420.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + // Small error is expected + testCase.mColor = BGRAColor(0x02, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent8bit422AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-8bit-yuv422.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + // Small error is expected + testCase.mColor = BGRAColor(0x02, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Transparent8bit444AVIFTestCase() { + auto testCase = + ImageTestCase("transparent-green-50pct-8bit-yuv444.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); + // Small error is expected + testCase.mColor = BGRAColor(0x02, 0xFF, 0x00, 0x80); + return testCase; +} + +ImageTestCase Gray8bitLimitedRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-limited-range-bt601.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitLimitedRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-limited-range-bt709.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitLimitedRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-limited-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitFullRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-full-range-bt601.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitFullRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-full-range-bt709.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitFullRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-full-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitLimitedRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-limited-range-bt601.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitLimitedRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-limited-range-bt709.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitLimitedRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-limited-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitFullRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-full-range-bt601.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitFullRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-full-range-bt709.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitFullRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-full-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitLimitedRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-limited-range-bt601.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitLimitedRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-limited-range-bt709.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitLimitedRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-limited-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitFullRangeBT601AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-full-range-bt601.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitFullRangeBT709AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-full-range-bt709.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitFullRangeBT2020AVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-full-range-bt2020.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitLimitedRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-limited-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray8bitFullRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-8bit-full-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitLimitedRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-limited-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray10bitFullRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-10bit-full-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitLimitedRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-limited-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + // Small error is expected + testCase.mColor = BGRAColor(0xEA, 0xEA, 0xEA, 0xFF); + return testCase; +} + +ImageTestCase Gray12bitFullRangeGrayscaleAVIFTestCase() { + auto testCase = ImageTestCase("gray-235-12bit-full-range-grayscale.avif", + "image/avif", IntSize(100, 100)); + testCase.mColor = BGRAColor(0xEB, 0xEB, 0xEB, 0xFF); + return testCase; +} + +ImageTestCase StackCheckAVIFTestCase() { + return ImageTestCase("stackcheck.avif", "image/avif", IntSize(4096, 2924), + TEST_CASE_IGNORE_OUTPUT); +} + +// Add TEST_CASE_IGNORE_OUTPUT since this isn't a solid green image and we just +// want to test that it decodes correctly. +ImageTestCase MultiLayerAVIFTestCase() { + return ImageTestCase("multilayer.avif", "image/avif", IntSize(1280, 720), + TEST_CASE_IGNORE_OUTPUT); +} + +ImageTestCase LargeWebPTestCase() { + return ImageTestCase("large.webp", "image/webp", IntSize(1200, 660), + TEST_CASE_IGNORE_OUTPUT); +} + +ImageTestCase LargeAVIFTestCase() { + return ImageTestCase("large.avif", "image/avif", IntSize(1200, 660), + TEST_CASE_IGNORE_OUTPUT); +} + +ImageTestCase GreenWebPIccSrgbTestCase() { + return ImageTestCase("green.icc_srgb.webp", "image/webp", IntSize(100, 100)); +} + +ImageTestCase GreenFirstFrameAnimatedGIFTestCase() { + return ImageTestCase("first-frame-green.gif", "image/gif", IntSize(100, 100), + TEST_CASE_IS_ANIMATED); +} + +ImageTestCase GreenFirstFrameAnimatedPNGTestCase() { + return ImageTestCase("first-frame-green.png", "image/png", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED); +} + +ImageTestCase GreenFirstFrameAnimatedWebPTestCase() { + return ImageTestCase("first-frame-green.webp", "image/webp", + IntSize(100, 100), TEST_CASE_IS_ANIMATED); +} + +ImageTestCase GreenFirstFrameAnimatedAVIFTestCase() { + return ImageTestCase("first-frame-green.avif", "image/avif", + IntSize(100, 100), TEST_CASE_IS_ANIMATED); +} + +ImageTestCase BlendAnimatedGIFTestCase() { + return ImageTestCase("blend.gif", "image/gif", IntSize(100, 100), + TEST_CASE_IS_ANIMATED); +} + +ImageTestCase BlendAnimatedPNGTestCase() { + return ImageTestCase("blend.png", "image/png", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED); +} + +ImageTestCase BlendAnimatedWebPTestCase() { + return ImageTestCase("blend.webp", "image/webp", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED); +} + +ImageTestCase BlendAnimatedAVIFTestCase() { + return ImageTestCase("blend.avif", "image/avif", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED); +} + +ImageTestCase CorruptTestCase() { + return ImageTestCase("corrupt.jpg", "image/jpeg", IntSize(100, 100), + TEST_CASE_HAS_ERROR); +} + +ImageTestCase CorruptBMPWithTruncatedHeader() { + // This BMP has a header which is truncated right between the BIH and the + // bitfields, which is a particularly error-prone place w.r.t. the BMP decoder + // state machine. + return ImageTestCase("invalid-truncated-metadata.bmp", "image/bmp", + IntSize(100, 100), TEST_CASE_HAS_ERROR); +} + +ImageTestCase CorruptICOWithBadBMPWidthTestCase() { + // This ICO contains a BMP icon which has a width that doesn't match the size + // listed in the corresponding ICO directory entry. + return ImageTestCase("corrupt-with-bad-bmp-width.ico", "image/x-icon", + IntSize(100, 100), TEST_CASE_HAS_ERROR); +} + +ImageTestCase CorruptICOWithBadBMPHeightTestCase() { + // This ICO contains a BMP icon which has a height that doesn't match the size + // listed in the corresponding ICO directory entry. + return ImageTestCase("corrupt-with-bad-bmp-height.ico", "image/x-icon", + IntSize(100, 100), TEST_CASE_HAS_ERROR); +} + +ImageTestCase CorruptICOWithBadBppTestCase() { + // This test case is an ICO with a BPP (15) in the ICO header which differs + // from that in the BMP header itself (1). It should ignore the ICO BPP when + // the BMP BPP is available and thus correctly decode the image. + return ImageTestCase("corrupt-with-bad-ico-bpp.ico", "image/x-icon", + IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase CorruptAVIFTestCase() { + return ImageTestCase("bug-1655846.avif", "image/avif", IntSize(100, 100), + TEST_CASE_HAS_ERROR); +} + +ImageTestCase TransparentAVIFTestCase() { + return ImageTestCase("transparent.avif", "image/avif", IntSize(1200, 1200), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase TransparentPNGTestCase() { + return ImageTestCase("transparent.png", "image/png", IntSize(32, 32), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase TransparentGIFTestCase() { + return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase TransparentWebPTestCase() { + ImageTestCase test("transparent.webp", "image/webp", IntSize(100, 100), + TEST_CASE_IS_TRANSPARENT); + test.mColor = BGRAColor::Transparent(); + return test; +} + +ImageTestCase TransparentNoAlphaHeaderWebPTestCase() { + ImageTestCase test("transparent-no-alpha-header.webp", "image/webp", + IntSize(100, 100), TEST_CASE_IS_FUZZY); + test.mColor = BGRAColor(0x00, 0x00, 0x00, 0xFF); // black + return test; +} + +ImageTestCase FirstFramePaddingGIFTestCase() { + return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase TransparentIfWithinICOBMPTestCase(TestCaseFlags aFlags) { + // This is a BMP that is only transparent when decoded as if it is within an + // ICO file. (Note: aFlags needs to be set to TEST_CASE_DEFAULT_FLAGS or + // TEST_CASE_IS_TRANSPARENT accordingly.) + return ImageTestCase("transparent-if-within-ico.bmp", "image/bmp", + IntSize(32, 32), aFlags); +} + +ImageTestCase RLE4BMPTestCase() { + return ImageTestCase("rle4.bmp", "image/bmp", IntSize(320, 240), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase RLE8BMPTestCase() { + return ImageTestCase("rle8.bmp", "image/bmp", IntSize(32, 32), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase NoFrameDelayGIFTestCase() { + // This is an invalid (or at least, questionably valid) GIF that's animated + // even though it specifies a frame delay of zero. It's animated, but it's not + // marked TEST_CASE_IS_ANIMATED because the metadata decoder can't detect that + // it's animated. + return ImageTestCase("no-frame-delay.gif", "image/gif", IntSize(100, 100)); +} + +ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase() { + // This is a corrupt GIF that has extra image sub blocks between the first and + // second frame. + return ImageTestCase("animated-with-extra-image-sub-blocks.gif", "image/gif", + IntSize(100, 100)); +} + +ImageTestCase DownscaledPNGTestCase() { + // This testcase (and all the other "downscaled") testcases) consists of 25 + // lines of green, followed by 25 lines of red, followed by 25 lines of green, + // followed by 25 more lines of red. It's intended that tests downscale it + // from 100x100 to 20x20, so we specify a 20x20 output size. + return ImageTestCase("downscaled.png", "image/png", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledGIFTestCase() { + return ImageTestCase("downscaled.gif", "image/gif", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledJPGTestCase() { + return ImageTestCase("downscaled.jpg", "image/jpeg", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledBMPTestCase() { + return ImageTestCase("downscaled.bmp", "image/bmp", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledICOTestCase() { + return ImageTestCase("downscaled.ico", "image/x-icon", IntSize(100, 100), + IntSize(20, 20), TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase DownscaledIconTestCase() { + return ImageTestCase("downscaled.icon", "image/icon", IntSize(100, 100), + IntSize(20, 20), TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase DownscaledWebPTestCase() { + return ImageTestCase("downscaled.webp", "image/webp", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledAVIFTestCase() { + return ImageTestCase("downscaled.avif", "image/avif", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase DownscaledTransparentICOWithANDMaskTestCase() { + // This test case is an ICO with AND mask transparency. We want to ensure that + // we can downscale it without crashing or triggering ASAN failures, but its + // content isn't simple to verify, so for now we don't check the output. + return ImageTestCase("transparent-ico-with-and-mask.ico", "image/x-icon", + IntSize(32, 32), IntSize(20, 20), + TEST_CASE_IS_TRANSPARENT | TEST_CASE_IGNORE_OUTPUT); +} + +ImageTestCase TruncatedSmallGIFTestCase() { + return ImageTestCase("green-1x1-truncated.gif", "image/gif", IntSize(1, 1), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase LargeICOWithBMPTestCase() { + return ImageTestCase("green-large-bmp.ico", "image/x-icon", IntSize(256, 256), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase LargeICOWithPNGTestCase() { + return ImageTestCase("green-large-png.ico", "image/x-icon", IntSize(512, 512), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase GreenMultipleSizesICOTestCase() { + return ImageTestCase("green-multiple-sizes.ico", "image/x-icon", + IntSize(256, 256)); +} + +ImageTestCase PerfGrayJPGTestCase() { + return ImageTestCase("perf_gray.jpg", "image/jpeg", IntSize(1000, 1000)); +} + +ImageTestCase PerfCmykJPGTestCase() { + return ImageTestCase("perf_cmyk.jpg", "image/jpeg", IntSize(1000, 1000)); +} + +ImageTestCase PerfYCbCrJPGTestCase() { + return ImageTestCase("perf_ycbcr.jpg", "image/jpeg", IntSize(1000, 1000)); +} + +ImageTestCase PerfRgbPNGTestCase() { + return ImageTestCase("perf_srgb.png", "image/png", IntSize(1000, 1000)); +} + +ImageTestCase PerfRgbAlphaPNGTestCase() { + return ImageTestCase("perf_srgb_alpha.png", "image/png", IntSize(1000, 1000), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase PerfGrayPNGTestCase() { + return ImageTestCase("perf_gray.png", "image/png", IntSize(1000, 1000)); +} + +ImageTestCase PerfGrayAlphaPNGTestCase() { + return ImageTestCase("perf_gray_alpha.png", "image/png", IntSize(1000, 1000), + TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase PerfRgbLosslessWebPTestCase() { + return ImageTestCase("perf_srgb_lossless.webp", "image/webp", + IntSize(1000, 1000)); +} + +ImageTestCase PerfRgbAlphaLosslessWebPTestCase() { + return ImageTestCase("perf_srgb_alpha_lossless.webp", "image/webp", + IntSize(1000, 1000), TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase PerfRgbLossyWebPTestCase() { + return ImageTestCase("perf_srgb_lossy.webp", "image/webp", + IntSize(1000, 1000)); +} + +ImageTestCase PerfRgbAlphaLossyWebPTestCase() { + return ImageTestCase("perf_srgb_alpha_lossy.webp", "image/webp", + IntSize(1000, 1000), TEST_CASE_IS_TRANSPARENT); +} + +ImageTestCase PerfRgbGIFTestCase() { + return ImageTestCase("perf_srgb.gif", "image/gif", IntSize(1000, 1000)); +} + +#ifdef MOZ_JXL +ImageTestCase GreenJXLTestCase() { + return ImageTestCase("green.jxl", "image/jxl", IntSize(100, 100)); +} + +ImageTestCase DownscaledJXLTestCase() { + return ImageTestCase("downscaled.jxl", "image/jxl", IntSize(100, 100), + IntSize(20, 20)); +} + +ImageTestCase LargeJXLTestCase() { + return ImageTestCase("large.jxl", "image/jxl", IntSize(1200, 660), + TEST_CASE_IGNORE_OUTPUT); +} + +ImageTestCase TransparentJXLTestCase() { + return ImageTestCase("transparent.jxl", "image/jxl", IntSize(1200, 1200), + TEST_CASE_IS_TRANSPARENT); +} +#endif + +ImageTestCase ExifResolutionTestCase() { + return ImageTestCase("exif_resolution.jpg", "image/jpeg", IntSize(100, 50)); +} + +RefPtr TestCaseToDecodedImage(const ImageTestCase& aTestCase) { + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(aTestCase.mMimeType)); + MOZ_RELEASE_ASSERT(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + MOZ_RELEASE_ASSERT(inputStream); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + // Use GetFrame() to force a sync decode of the image. + RefPtr surface = image->GetFrame( + imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_SYNC_DECODE); + Unused << surface; + return image; +} + +} // namespace image +} // namespace mozilla diff --git a/image/test/gtest/Common.h b/image/test/gtest/Common.h new file mode 100644 index 0000000000..bd6ae64a42 --- /dev/null +++ b/image/test/gtest/Common.h @@ -0,0 +1,600 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_image_test_gtest_Common_h +#define mozilla_image_test_gtest_Common_h + +#include + +#include "gtest/gtest.h" + +#include "mozilla/Attributes.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/gfx/2D.h" +#include "Decoder.h" +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "nsCOMPtr.h" +#include "SurfaceFlags.h" +#include "SurfacePipe.h" +#include "SurfacePipeFactory.h" + +class nsIInputStream; + +namespace mozilla { +namespace image { + +/////////////////////////////////////////////////////////////////////////////// +// Types +/////////////////////////////////////////////////////////////////////////////// + +struct BGRAColor { + BGRAColor() : BGRAColor(0, 0, 0, 0) {} + + BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha, + bool aPremultiplied = false, bool asRGB = true) + : mBlue(aBlue), + mGreen(aGreen), + mRed(aRed), + mAlpha(aAlpha), + mPremultiplied(aPremultiplied), + msRGB(asRGB) {} + + static BGRAColor Green() { return BGRAColor(0x00, 0xFF, 0x00, 0xFF); } + static BGRAColor Red() { return BGRAColor(0x00, 0x00, 0xFF, 0xFF); } + static BGRAColor Blue() { return BGRAColor(0xFF, 0x00, 0x00, 0xFF); } + static BGRAColor Transparent() { return BGRAColor(0x00, 0x00, 0x00, 0x00); } + + static BGRAColor FromPixel(uint32_t aPixel) { + uint8_t r, g, b, a; + r = (aPixel >> gfx::SurfaceFormatBit::OS_R) & 0xFF; + g = (aPixel >> gfx::SurfaceFormatBit::OS_G) & 0xFF; + b = (aPixel >> gfx::SurfaceFormatBit::OS_B) & 0xFF; + a = (aPixel >> gfx::SurfaceFormatBit::OS_A) & 0xFF; + return BGRAColor(b, g, r, a, true); + } + + BGRAColor DeviceColor() const { + MOZ_ASSERT(!mPremultiplied); + if (msRGB) { + gfx::DeviceColor color = gfx::ToDeviceColor( + gfx::sRGBColor(float(mRed) / 255.0f, float(mGreen) / 255.0f, + float(mBlue) / 255.0f, 1.0)); + return BGRAColor(uint8_t(color.b * 255.0f), uint8_t(color.g * 255.0f), + uint8_t(color.r * 255.0f), mAlpha, mPremultiplied, + /* asRGB */ false); + } + return *this; + } + + BGRAColor sRGBColor() const { + MOZ_ASSERT(msRGB); + MOZ_ASSERT(!mPremultiplied); + return *this; + } + + BGRAColor Premultiply() const { + if (!mPremultiplied) { + return BGRAColor(gfxPreMultiply(mBlue, mAlpha), + gfxPreMultiply(mGreen, mAlpha), + gfxPreMultiply(mRed, mAlpha), mAlpha, true); + } + return *this; + } + + uint32_t AsPixel() const { + if (!mPremultiplied) { + return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue); + } + return gfxPackedPixelNoPreMultiply(mAlpha, mRed, mGreen, mBlue); + } + + uint8_t mBlue; + uint8_t mGreen; + uint8_t mRed; + uint8_t mAlpha; + bool mPremultiplied; + bool msRGB; +}; + +enum TestCaseFlags { + TEST_CASE_DEFAULT_FLAGS = 0, + TEST_CASE_IS_FUZZY = 1 << 0, + TEST_CASE_HAS_ERROR = 1 << 1, + TEST_CASE_IS_TRANSPARENT = 1 << 2, + TEST_CASE_IS_ANIMATED = 1 << 3, + TEST_CASE_IGNORE_OUTPUT = 1 << 4, + TEST_CASE_ASSUME_SRGB_OUTPUT = 1 << 5, +}; + +struct ImageTestCase { + ImageTestCase(const char* aPath, const char* aMimeType, gfx::IntSize aSize, + uint32_t aFlags = TEST_CASE_DEFAULT_FLAGS) + : mPath(aPath), + mMimeType(aMimeType), + mSize(aSize), + mOutputSize(aSize), + mFlags(aFlags), + mSurfaceFlags(DefaultSurfaceFlags()), + mColor(BGRAColor::Green()) {} + + ImageTestCase(const char* aPath, const char* aMimeType, gfx::IntSize aSize, + gfx::IntSize aOutputSize, + uint32_t aFlags = TEST_CASE_DEFAULT_FLAGS) + : mPath(aPath), + mMimeType(aMimeType), + mSize(aSize), + mOutputSize(aOutputSize), + mFlags(aFlags), + mSurfaceFlags(DefaultSurfaceFlags()), + mColor(BGRAColor::Green()) {} + + ImageTestCase WithSurfaceFlags(SurfaceFlags aSurfaceFlags) const { + ImageTestCase self = *this; + self.mSurfaceFlags = aSurfaceFlags; + return self; + } + + ImageTestCase WithFlags(uint32_t aFlags) const { + ImageTestCase self = *this; + self.mFlags = aFlags; + return self; + } + + BGRAColor ChooseColor(const BGRAColor& aColor) const { + // If we are forcing the output to be sRGB via the surface flag, or the + // test case is marked as assuming sRGB (used when the image itself is not + // explicitly tagged, and as a result, imagelib won't perform any color + // conversion), we should use the sRGB presentation of the color. + if ((mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) || + (mFlags & TEST_CASE_ASSUME_SRGB_OUTPUT)) { + return aColor.sRGBColor(); + } + return aColor.DeviceColor(); + } + + BGRAColor Color() const { return ChooseColor(mColor); } + + uint8_t Fuzz() const { + // If we are using device space, there can easily be off by 1 channel errors + // depending on the color profile and how the rounding went. + if (mFlags & TEST_CASE_IS_FUZZY || + !(mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE)) { + return 1; + } + return 0; + } + + const char* mPath; + const char* mMimeType; + gfx::IntSize mSize; + gfx::IntSize mOutputSize; + uint32_t mFlags; + SurfaceFlags mSurfaceFlags; + BGRAColor mColor; +}; + +/////////////////////////////////////////////////////////////////////////////// +// General Helpers +/////////////////////////////////////////////////////////////////////////////// + +/** + * A RAII class that ensure that ImageLib services are available. Any tests that + * require ImageLib to be initialized (for example, any test that uses the + * SurfaceCache; see image::EnsureModuleInitialized() for the full list) can + * use this class to ensure that ImageLib services are available. Failure to do + * so can result in strange, non-deterministic failures. + */ +class AutoInitializeImageLib { + public: + AutoInitializeImageLib(); +}; + +/** + * A test fixture class used for benchmark tests. It preloads the image data + * from disk to avoid including that in the timing. + */ +class ImageBenchmarkBase : public ::testing::Test { + protected: + ImageBenchmarkBase(const ImageTestCase& aTestCase) : mTestCase(aTestCase) {} + + void SetUp() override; + void TearDown() override; + + AutoInitializeImageLib mInit; + ImageTestCase mTestCase; + RefPtr mSourceBuffer; +}; + +/// Spins on the main thread to process any pending events. +void SpinPendingEvents(); + +/// Loads a file from the current directory. @return an nsIInputStream for it. +already_AddRefed LoadFile(const char* aRelativePath); + +/** + * @returns true if every pixel of @aSurface is @aColor. + * + * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color + * component. This may be necessary for tests that involve JPEG images or + * downscaling. + */ +bool IsSolidColor(gfx::SourceSurface* aSurface, BGRAColor aColor, + uint8_t aFuzz = 0); + +/** + * @returns true if every pixel in the range of rows specified by @aStartRow and + * @aRowCount of @aSurface is @aColor. + * + * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color + * component. This may be necessary for tests that involve JPEG images or + * downscaling. + */ +bool RowsAreSolidColor(gfx::SourceSurface* aSurface, int32_t aStartRow, + int32_t aRowCount, BGRAColor aColor, uint8_t aFuzz = 0); + +/** + * @returns true if every pixel in the rect specified by @aRect is @aColor. + * + * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color + * component. This may be necessary for tests that involve JPEG images or + * downscaling. + */ +bool RectIsSolidColor(gfx::SourceSurface* aSurface, const gfx::IntRect& aRect, + BGRAColor aColor, uint8_t aFuzz = 0); + +/** + * @returns true if the pixels in @aRow of @aSurface match the pixels given in + * @aPixels. + */ +bool RowHasPixels(gfx::SourceSurface* aSurface, int32_t aRow, + const std::vector& aPixels); + +// ExpectNoResume is an IResumable implementation for use by tests that expect +// Resume() to never get called. +class ExpectNoResume final : public IResumable { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExpectNoResume, override) + + void Resume() override { FAIL() << "Resume() should not get called"; } + + private: + ~ExpectNoResume() override {} +}; + +// CountResumes is an IResumable implementation for use by tests that expect +// Resume() to get called a certain number of times. +class CountResumes : public IResumable { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CountResumes, override) + + CountResumes() : mCount(0) {} + + void Resume() override { mCount++; } + uint32_t Count() const { return mCount; } + + private: + ~CountResumes() override {} + + uint32_t mCount; +}; + +/////////////////////////////////////////////////////////////////////////////// +// SurfacePipe Helpers +/////////////////////////////////////////////////////////////////////////////// + +/** + * Creates a decoder with no data associated with, suitable for testing code + * that requires a decoder to initialize or to allocate surfaces but doesn't + * actually need the decoder to do any decoding. + * + * XXX(seth): We only need this because SurfaceSink defer to the decoder for + * surface allocation. Once all decoders use SurfacePipe we won't need to do + * that anymore and we can remove this function. + */ +already_AddRefed CreateTrivialDecoder(); + +/** + * Creates a pipeline of SurfaceFilters from a list of Config structs and passes + * it to the provided lambda @aFunc. Assertions that the pipeline is constructly + * correctly and cleanup of any allocated surfaces is handled automatically. + * + * @param aDecoder The decoder to use for allocating surfaces. + * @param aFunc The lambda function to pass the filter pipeline to. + * @param aConfigs The configuration for the pipeline. + */ +template +void WithFilterPipeline(Decoder* aDecoder, Func aFunc, bool aFinish, + const Configs&... aConfigs) { + auto pipe = MakeUnique::Type>(); + nsresult rv = pipe->Configure(aConfigs...); + ASSERT_NS_SUCCEEDED(rv); + + aFunc(aDecoder, pipe.get()); + + if (aFinish) { + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + if (currentFrame) { + currentFrame->Finish(); + } + } +} + +template +void WithFilterPipeline(Decoder* aDecoder, Func aFunc, + const Configs&... aConfigs) { + WithFilterPipeline(aDecoder, aFunc, true, aConfigs...); +} + +/** + * Creates a pipeline of SurfaceFilters from a list of Config structs and + * asserts that configuring it fails. Cleanup of any allocated surfaces is + * handled automatically. + * + * @param aDecoder The decoder to use for allocating surfaces. + * @param aConfigs The configuration for the pipeline. + */ +template +void AssertConfiguringPipelineFails(Decoder* aDecoder, + const Configs&... aConfigs) { + auto pipe = MakeUnique::Type>(); + nsresult rv = pipe->Configure(aConfigs...); + + // Callers expect configuring the pipeline to fail. + ASSERT_NS_FAILED(rv); + + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + if (currentFrame) { + currentFrame->Finish(); + } +} + +/** + * Asserts that the provided filter pipeline is in the correct final state, + * which is to say, the entire surface has been written to (IsSurfaceFinished() + * returns true) and the invalid rects are as expected. + * + * @param aFilter The filter pipeline to check. + * @param aInputSpaceRect The expect invalid rect, in input space. + * @param aoutputSpaceRect The expect invalid rect, in output space. + */ +void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter, + const gfx::IntRect& aInputSpaceRect, + const gfx::IntRect& aOutputSpaceRect); + +/** + * Checks a generated image for correctness. Reports any unexpected deviation + * from the expected image as GTest failures. + * + * @param aDecoder The decoder which contains the image. The decoder's current + * frame will be checked. + * @param aRect The region in the space of the output surface that the filter + * pipeline will actually write to. It's expected that pixels in + * this region are green, while pixels outside this region are + * transparent. + * @param aFuzz The amount of fuzz to use in pixel comparisons. + */ +void CheckGeneratedImage(Decoder* aDecoder, const gfx::IntRect& aRect, + uint8_t aFuzz = 0); + +/** + * Checks a generated surface for correctness. Reports any unexpected deviation + * from the expected image as GTest failures. + * + * @param aSurface The surface to check. + * @param aRect The region in the space of the output surface that the filter + * pipeline will actually write to. + * @param aInnerColor Check that pixels inside of aRect are this color. + * @param aOuterColor Check that pixels outside of aRect are this color. + * @param aFuzz The amount of fuzz to use in pixel comparisons. + */ +void CheckGeneratedSurface(gfx::SourceSurface* aSurface, + const gfx::IntRect& aRect, + const BGRAColor& aInnerColor, + const BGRAColor& aOuterColor, uint8_t aFuzz = 0); + +/** + * Tests the result of calling WritePixels() using the provided SurfaceFilter + * pipeline. The pipeline must be a normal (i.e., non-paletted) pipeline. + * + * The arguments are specified in the an order intended to minimize the number + * of arguments that most test cases need to pass. + * + * @param aDecoder The decoder whose current frame will be written to. + * @param aFilter The SurfaceFilter pipeline to use. + * @param aOutputRect The region in the space of the output surface that will be + * invalidated by the filter pipeline. Defaults to + * (0, 0, 100, 100). + * @param aInputRect The region in the space of the input image that will be + * invalidated by the filter pipeline. Defaults to + * (0, 0, 100, 100). + * @param aInputWriteRect The region in the space of the input image that the + * filter pipeline will allow writes to. Note the + * difference from @aInputRect: @aInputRect is the actual + * region invalidated, while @aInputWriteRect is the + * region that is written to. These can differ in cases + * where the input is not clipped to the size of the + * image. Defaults to the entire input rect. + * @param aOutputWriteRect The region in the space of the output surface that + * the filter pipeline will actually write to. It's + * expected that pixels in this region are green, while + * pixels outside this region are transparent. Defaults + * to the entire output rect. + */ +void CheckWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter, + const Maybe& aOutputRect = Nothing(), + const Maybe& aInputRect = Nothing(), + const Maybe& aInputWriteRect = Nothing(), + const Maybe& aOutputWriteRect = Nothing(), + uint8_t aFuzz = 0); + +/** + * Tests the result of calling WritePixels() using the provided SurfaceFilter + * pipeline. Allows for control over the input color to write, and the expected + * output color. + * @see CheckWritePixels() for documentation of the arguments. + */ +void CheckTransformedWritePixels( + Decoder* aDecoder, SurfaceFilter* aFilter, const BGRAColor& aInputColor, + const BGRAColor& aOutputColor, + const Maybe& aOutputRect = Nothing(), + const Maybe& aInputRect = Nothing(), + const Maybe& aInputWriteRect = Nothing(), + const Maybe& aOutputWriteRect = Nothing(), uint8_t aFuzz = 0); + +/////////////////////////////////////////////////////////////////////////////// +// Decoder Helpers +/////////////////////////////////////////////////////////////////////////////// + +// Friend class of Decoder to access internals for tests. +class MOZ_STACK_CLASS DecoderTestHelper final { + public: + explicit DecoderTestHelper(Decoder* aDecoder) : mDecoder(aDecoder) {} + + void PostIsAnimated(FrameTimeout aTimeout) { + mDecoder->PostIsAnimated(aTimeout); + } + + void PostFrameStop(Opacity aOpacity) { mDecoder->PostFrameStop(aOpacity); } + + private: + Decoder* mDecoder; +}; + +/////////////////////////////////////////////////////////////////////////////// +// Test Data +/////////////////////////////////////////////////////////////////////////////// + +ImageTestCase GreenPNGTestCase(); +ImageTestCase GreenGIFTestCase(); +ImageTestCase GreenJPGTestCase(); +ImageTestCase GreenBMPTestCase(); +ImageTestCase GreenICOTestCase(); +ImageTestCase GreenIconTestCase(); +ImageTestCase GreenWebPTestCase(); +ImageTestCase GreenAVIFTestCase(); + +ImageTestCase NonzeroReservedAVIFTestCase(); +ImageTestCase MultipleColrAVIFTestCase(); +ImageTestCase Transparent10bit420AVIFTestCase(); +ImageTestCase Transparent10bit422AVIFTestCase(); +ImageTestCase Transparent10bit444AVIFTestCase(); +ImageTestCase Transparent12bit420AVIFTestCase(); +ImageTestCase Transparent12bit422AVIFTestCase(); +ImageTestCase Transparent12bit444AVIFTestCase(); +ImageTestCase Transparent8bit420AVIFTestCase(); +ImageTestCase Transparent8bit422AVIFTestCase(); +ImageTestCase Transparent8bit444AVIFTestCase(); + +ImageTestCase Gray8bitLimitedRangeBT601AVIFTestCase(); +ImageTestCase Gray8bitLimitedRangeBT709AVIFTestCase(); +ImageTestCase Gray8bitLimitedRangeBT2020AVIFTestCase(); +ImageTestCase Gray8bitFullRangeBT601AVIFTestCase(); +ImageTestCase Gray8bitFullRangeBT709AVIFTestCase(); +ImageTestCase Gray8bitFullRangeBT2020AVIFTestCase(); +ImageTestCase Gray10bitLimitedRangeBT601AVIFTestCase(); +ImageTestCase Gray10bitLimitedRangeBT709AVIFTestCase(); +ImageTestCase Gray10bitLimitedRangeBT2020AVIFTestCase(); +ImageTestCase Gray10bitFullRangeBT601AVIFTestCase(); +ImageTestCase Gray10bitFullRangeBT709AVIFTestCase(); +ImageTestCase Gray10bitFullRangeBT2020AVIFTestCase(); +ImageTestCase Gray12bitLimitedRangeBT601AVIFTestCase(); +ImageTestCase Gray12bitLimitedRangeBT709AVIFTestCase(); +ImageTestCase Gray12bitLimitedRangeBT2020AVIFTestCase(); +ImageTestCase Gray12bitFullRangeBT601AVIFTestCase(); +ImageTestCase Gray12bitFullRangeBT709AVIFTestCase(); +ImageTestCase Gray12bitFullRangeBT2020AVIFTestCase(); +ImageTestCase Gray8bitLimitedRangeGrayscaleAVIFTestCase(); +ImageTestCase Gray8bitFullRangeGrayscaleAVIFTestCase(); +ImageTestCase Gray10bitLimitedRangeGrayscaleAVIFTestCase(); +ImageTestCase Gray10bitFullRangeGrayscaleAVIFTestCase(); +ImageTestCase Gray12bitLimitedRangeGrayscaleAVIFTestCase(); +ImageTestCase Gray12bitFullRangeGrayscaleAVIFTestCase(); + +ImageTestCase StackCheckAVIFTestCase(); + +ImageTestCase LargeWebPTestCase(); +ImageTestCase GreenWebPIccSrgbTestCase(); + +ImageTestCase GreenFirstFrameAnimatedGIFTestCase(); +ImageTestCase GreenFirstFrameAnimatedPNGTestCase(); +ImageTestCase GreenFirstFrameAnimatedWebPTestCase(); +ImageTestCase GreenFirstFrameAnimatedAVIFTestCase(); + +ImageTestCase BlendAnimatedGIFTestCase(); +ImageTestCase BlendAnimatedPNGTestCase(); +ImageTestCase BlendAnimatedWebPTestCase(); +ImageTestCase BlendAnimatedAVIFTestCase(); + +ImageTestCase CorruptTestCase(); +ImageTestCase CorruptBMPWithTruncatedHeader(); +ImageTestCase CorruptICOWithBadBMPWidthTestCase(); +ImageTestCase CorruptICOWithBadBMPHeightTestCase(); +ImageTestCase CorruptICOWithBadBppTestCase(); + +ImageTestCase TransparentPNGTestCase(); +ImageTestCase TransparentGIFTestCase(); +ImageTestCase TransparentWebPTestCase(); +ImageTestCase TransparentNoAlphaHeaderWebPTestCase(); +ImageTestCase FirstFramePaddingGIFTestCase(); +ImageTestCase TransparentIfWithinICOBMPTestCase(TestCaseFlags aFlags); +ImageTestCase NoFrameDelayGIFTestCase(); +ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase(); + +ImageTestCase TransparentBMPWhenBMPAlphaEnabledTestCase(); +ImageTestCase RLE4BMPTestCase(); +ImageTestCase RLE8BMPTestCase(); + +ImageTestCase DownscaledPNGTestCase(); +ImageTestCase DownscaledGIFTestCase(); +ImageTestCase DownscaledJPGTestCase(); +ImageTestCase DownscaledBMPTestCase(); +ImageTestCase DownscaledICOTestCase(); +ImageTestCase DownscaledIconTestCase(); +ImageTestCase DownscaledWebPTestCase(); +ImageTestCase DownscaledTransparentICOWithANDMaskTestCase(); + +ImageTestCase TruncatedSmallGIFTestCase(); + +ImageTestCase LargeICOWithBMPTestCase(); +ImageTestCase LargeICOWithPNGTestCase(); +ImageTestCase GreenMultipleSizesICOTestCase(); + +ImageTestCase PerfGrayJPGTestCase(); +ImageTestCase PerfCmykJPGTestCase(); +ImageTestCase PerfYCbCrJPGTestCase(); +ImageTestCase PerfRgbPNGTestCase(); +ImageTestCase PerfRgbAlphaPNGTestCase(); +ImageTestCase PerfGrayPNGTestCase(); +ImageTestCase PerfGrayAlphaPNGTestCase(); +ImageTestCase PerfRgbLosslessWebPTestCase(); +ImageTestCase PerfRgbAlphaLosslessWebPTestCase(); +ImageTestCase PerfRgbLossyWebPTestCase(); +ImageTestCase PerfRgbAlphaLossyWebPTestCase(); +ImageTestCase PerfRgbGIFTestCase(); + +ImageTestCase CorruptAVIFTestCase(); +ImageTestCase DownscaledAVIFTestCase(); +ImageTestCase LargeAVIFTestCase(); +ImageTestCase MultiLayerAVIFTestCase(); +ImageTestCase TransparentAVIFTestCase(); + +#ifdef MOZ_JXL +ImageTestCase GreenJXLTestCase(); +ImageTestCase DownscaledJXLTestCase(); +ImageTestCase LargeJXLTestCase(); +ImageTestCase TransparentJXLTestCase(); +#endif + +ImageTestCase ExifResolutionTestCase(); + +RefPtr TestCaseToDecodedImage(const ImageTestCase&); + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_test_gtest_Common_h diff --git a/image/test/gtest/TestADAM7InterpolatingFilter.cpp b/image/test/gtest/TestADAM7InterpolatingFilter.cpp new file mode 100644 index 0000000000..b2ae6b5a58 --- /dev/null +++ b/image/test/gtest/TestADAM7InterpolatingFilter.cpp @@ -0,0 +1,595 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "mozilla/Maybe.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +using std::generate; +using std::vector; + +template +void WithADAM7InterpolatingFilter(const IntSize& aSize, Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(bool(decoder)); + + WithFilterPipeline( + decoder, std::forward(aFunc), ADAM7InterpolatingConfig{}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +void AssertConfiguringADAM7InterpolatingFilterFails(const IntSize& aSize) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(bool(decoder)); + + AssertConfiguringPipelineFails( + decoder, ADAM7InterpolatingConfig{}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +uint8_t InterpolateByte(uint8_t aByteA, uint8_t aByteB, float aWeight) { + return uint8_t(aByteA * aWeight + aByteB * (1.0f - aWeight)); +} + +BGRAColor InterpolateColors(BGRAColor aColor1, BGRAColor aColor2, + float aWeight) { + return BGRAColor(InterpolateByte(aColor1.mBlue, aColor2.mBlue, aWeight), + InterpolateByte(aColor1.mGreen, aColor2.mGreen, aWeight), + InterpolateByte(aColor1.mRed, aColor2.mRed, aWeight), + InterpolateByte(aColor1.mAlpha, aColor2.mAlpha, aWeight)); +} + +enum class ShouldInterpolate { eYes, eNo }; + +BGRAColor HorizontallyInterpolatedPixel(uint32_t aCol, uint32_t aWidth, + const vector& aWeights, + ShouldInterpolate aShouldInterpolate, + const vector& aColors) { + // We cycle through the vector of weights forever. + float weight = aWeights[aCol % aWeights.size()]; + + // Find the columns of the two final pixels for this set of weights. + uint32_t finalPixel1 = aCol - aCol % aWeights.size(); + uint32_t finalPixel2 = finalPixel1 + aWeights.size(); + + // If |finalPixel2| is past the end of the row, that means that there is no + // final pixel after the pixel at |finalPixel1|. In that case, we just want to + // duplicate |finalPixel1|'s color until the end of the row. We can do that by + // setting |finalPixel2| equal to |finalPixel1| so that the interpolation has + // no effect. + if (finalPixel2 >= aWidth) { + finalPixel2 = finalPixel1; + } + + // We cycle through the vector of colors forever (subject to the above + // constraint about the end of the row). + BGRAColor color1 = aColors[finalPixel1 % aColors.size()]; + BGRAColor color2 = aColors[finalPixel2 % aColors.size()]; + + // If we're not interpolating, we treat all pixels which aren't final as + // transparent. Since the number of weights we have is equal to the stride + // between final pixels, we can check if |aCol| is a final pixel by checking + // whether |aCol| is a multiple of |aWeights.size()|. + if (aShouldInterpolate == ShouldInterpolate::eNo) { + return aCol % aWeights.size() == 0 ? color1 : BGRAColor::Transparent(); + } + + // Interpolate. + return InterpolateColors(color1, color2, weight); +} + +vector& InterpolationWeights(int32_t aStride) { + // Precalculated interpolation weights. These are used to interpolate + // between final pixels or between important rows. Although no interpolation + // is actually applied to the previous final pixel or important row value, + // the arrays still start with 1.0f, which is always skipped, primarily + // because otherwise |stride1Weights| would have zero elements. + static vector stride8Weights = {1.0f, 7 / 8.0f, 6 / 8.0f, + 5 / 8.0f, 4 / 8.0f, 3 / 8.0f, + 2 / 8.0f, 1 / 8.0f}; + static vector stride4Weights = {1.0f, 3 / 4.0f, 2 / 4.0f, 1 / 4.0f}; + static vector stride2Weights = {1.0f, 1 / 2.0f}; + static vector stride1Weights = {1.0f}; + + switch (aStride) { + case 8: + return stride8Weights; + case 4: + return stride4Weights; + case 2: + return stride2Weights; + case 1: + return stride1Weights; + default: + MOZ_CRASH(); + } +} + +int32_t ImportantRowStride(uint8_t aPass) { + // The stride between important rows for each pass, with a dummy value for + // the nonexistent pass 0 and for pass 8, since the tests run an extra pass to + // make sure nothing breaks. + static int32_t strides[] = {1, 8, 8, 4, 4, 2, 2, 1, 1}; + + return strides[aPass]; +} + +size_t FinalPixelStride(uint8_t aPass) { + // The stride between the final pixels in important rows for each pass, with + // a dummy value for the nonexistent pass 0 and for pass 8, since the tests + // run an extra pass to make sure nothing breaks. + static size_t strides[] = {1, 8, 4, 4, 2, 2, 1, 1, 1}; + + return strides[aPass]; +} + +bool IsImportantRow(int32_t aRow, uint8_t aPass) { + return aRow % ImportantRowStride(aPass) == 0; +} + +/** + * ADAM7 breaks up the image into 8x8 blocks. On each of the 7 passes, a new + * set of pixels in each block receives their final values, according to the + * following pattern: + * + * 1 6 4 6 2 6 4 6 + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * 3 6 4 6 3 6 4 6 + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * + * This function produces a row of pixels @aWidth wide, suitable for testing + * horizontal interpolation on pass @aPass. The pattern of pixels used is + * determined by @aPass and @aRow, which determine which pixels are final + * according to the table above, and @aColors, from which the pixel values + * are selected. + * + * There are two different behaviors: if |eNo| is passed for + * @aShouldInterpolate, non-final pixels are treated as transparent. If |eNo| + * is passed, non-final pixels get interpolated in from the surrounding final + * pixels. The intention is that |eNo| is passed to generate input which will + * be run through ADAM7InterpolatingFilter, and |eYes| is passed to generate + * reference data to check that the filter is performing horizontal + * interpolation correctly. + * + * This function does not perform vertical interpolation. Rows which aren't on + * the current pass are filled with transparent pixels. + * + * @return a vector representing a row of pixels. + */ +vector ADAM7HorizontallyInterpolatedRow( + uint8_t aPass, uint32_t aRow, uint32_t aWidth, + ShouldInterpolate aShouldInterpolate, const vector& aColors) { + EXPECT_GT(aPass, 0); + EXPECT_LE(aPass, 8); + EXPECT_GT(aColors.size(), 0u); + + vector result(aWidth); + + if (IsImportantRow(aRow, aPass)) { + vector& weights = InterpolationWeights(FinalPixelStride(aPass)); + + // Compute the horizontally interpolated row. + uint32_t col = 0; + generate(result.begin(), result.end(), [&] { + return HorizontallyInterpolatedPixel(col++, aWidth, weights, + aShouldInterpolate, aColors); + }); + } else { + // This is an unimportant row; just make the entire thing transparent. + generate(result.begin(), result.end(), + [] { return BGRAColor::Transparent(); }); + } + + EXPECT_EQ(result.size(), size_t(aWidth)); + + return result; +} + +WriteState WriteUninterpolatedPixels(SurfaceFilter* aFilter, + const IntSize& aSize, uint8_t aPass, + const vector& aColors) { + WriteState result = WriteState::NEED_MORE_DATA; + + for (int32_t row = 0; row < aSize.height; ++row) { + // Compute uninterpolated pixels for this row. + vector pixels = ADAM7HorizontallyInterpolatedRow( + aPass, row, aSize.width, ShouldInterpolate::eNo, aColors); + + // Write them to the surface. + auto pixelIterator = pixels.cbegin(); + result = aFilter->WritePixelsToRow( + [&] { return AsVariant((*pixelIterator++).AsPixel()); }); + + if (result != WriteState::NEED_MORE_DATA) { + break; + } + } + + return result; +} + +bool CheckHorizontallyInterpolatedImage(image::Decoder* aDecoder, + const IntSize& aSize, uint8_t aPass, + const vector& aColors) { + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (int32_t row = 0; row < aSize.height; ++row) { + if (!IsImportantRow(row, aPass)) { + continue; // Don't check rows which aren't important on this pass. + } + + // Compute the expected pixels, *with* interpolation to match what the + // filter should have done. + vector expectedPixels = ADAM7HorizontallyInterpolatedRow( + aPass, row, aSize.width, ShouldInterpolate::eYes, aColors); + + if (!RowHasPixels(surface, row, expectedPixels)) { + return false; + } + } + + return true; +} + +void CheckHorizontalInterpolation(const IntSize& aSize, + const vector& aColors) { + const IntRect surfaceRect(IntPoint(0, 0), aSize); + + WithADAM7InterpolatingFilter( + aSize, [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // We check horizontal interpolation behavior for each pass + // individually. In addition to the normal 7 passes that ADAM7 includes, + // we also check an eighth pass to verify that nothing breaks if extra + // data is written. + for (uint8_t pass = 1; pass <= 8; ++pass) { + // Write our color pattern to the surface. We don't perform any + // interpolation when writing to the filter so that we can check that + // the filter itself *does*. + WriteState result = + WriteUninterpolatedPixels(aFilter, aSize, pass, aColors); + + EXPECT_EQ(WriteState::FINISHED, result); + AssertCorrectPipelineFinalState(aFilter, surfaceRect, surfaceRect); + + // Check that the generated image matches the expected pattern, with + // interpolation applied. + EXPECT_TRUE(CheckHorizontallyInterpolatedImage(aDecoder, aSize, pass, + aColors)); + + // Prepare for the next pass. + aFilter->ResetToFirstRow(); + } + }); +} + +BGRAColor ADAM7RowColor(int32_t aRow, uint8_t aPass, + const vector& aColors) { + EXPECT_LT(0, aPass); + EXPECT_GE(8, aPass); + EXPECT_LT(0u, aColors.size()); + + // If this is an important row, select the color from the provided vector of + // colors, which we cycle through infinitely. If not, just fill the row with + // transparent pixels. + return IsImportantRow(aRow, aPass) ? aColors[aRow % aColors.size()] + : BGRAColor::Transparent(); +} + +WriteState WriteRowColorPixels(SurfaceFilter* aFilter, const IntSize& aSize, + uint8_t aPass, + const vector& aColors) { + WriteState result = WriteState::NEED_MORE_DATA; + + for (int32_t row = 0; row < aSize.height; ++row) { + const uint32_t color = ADAM7RowColor(row, aPass, aColors).AsPixel(); + + // Fill the surface with |color| pixels. + result = + aFilter->WritePixelsToRow([&] { return AsVariant(color); }); + + if (result != WriteState::NEED_MORE_DATA) { + break; + } + } + + return result; +} + +bool CheckVerticallyInterpolatedImage(image::Decoder* aDecoder, + const IntSize& aSize, uint8_t aPass, + const vector& aColors) { + vector& weights = InterpolationWeights(ImportantRowStride(aPass)); + + for (int32_t row = 0; row < aSize.height; ++row) { + // Vertically interpolation takes place between two important rows. The + // separation between the important rows is determined by the stride of this + // pass. When there is no "next" important row because we'd run off the + // bottom of the image, we use the same row for both. This matches + // ADAM7InterpolatingFilter's behavior of duplicating the last important row + // since there isn't another important row to vertically interpolate it + // with. + const int32_t stride = ImportantRowStride(aPass); + const int32_t prevImportantRow = row - row % stride; + const int32_t maybeNextImportantRow = prevImportantRow + stride; + const int32_t nextImportantRow = maybeNextImportantRow < aSize.height + ? maybeNextImportantRow + : prevImportantRow; + + // Retrieve the colors for the important rows we're going to interpolate. + const BGRAColor prevImportantRowColor = + ADAM7RowColor(prevImportantRow, aPass, aColors); + const BGRAColor nextImportantRowColor = + ADAM7RowColor(nextImportantRow, aPass, aColors); + + // The weight we'll use for interpolation is also determined by the stride. + // A row halfway between two important rows should have pixels that have a + // 50% contribution from each of the important rows, for example. + const float weight = weights[row % stride]; + const BGRAColor interpolatedColor = + InterpolateColors(prevImportantRowColor, nextImportantRowColor, weight); + + // Generate a row of expected pixels. Every pixel in the row is always the + // same color since we're only testing vertical interpolation between + // solid-colored rows. + vector expectedPixels(aSize.width); + generate(expectedPixels.begin(), expectedPixels.end(), + [&] { return interpolatedColor; }); + + // Check that the pixels match. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + if (!RowHasPixels(surface, row, expectedPixels)) { + return false; + } + } + + return true; +} + +void CheckVerticalInterpolation(const IntSize& aSize, + const vector& aColors) { + const IntRect surfaceRect(IntPoint(0, 0), aSize); + + WithADAM7InterpolatingFilter(aSize, [&](image::Decoder* aDecoder, + SurfaceFilter* aFilter) { + for (uint8_t pass = 1; pass <= 8; ++pass) { + // Write a pattern of rows to the surface. Important rows will receive a + // color selected from |aColors|; unimportant rows will be transparent. + WriteState result = WriteRowColorPixels(aFilter, aSize, pass, aColors); + + EXPECT_EQ(WriteState::FINISHED, result); + AssertCorrectPipelineFinalState(aFilter, surfaceRect, surfaceRect); + + // Check that the generated image matches the expected pattern, with + // interpolation applied. + EXPECT_TRUE( + CheckVerticallyInterpolatedImage(aDecoder, aSize, pass, aColors)); + + // Prepare for the next pass. + aFilter->ResetToFirstRow(); + } + }); +} + +void CheckInterpolation(const IntSize& aSize, + const vector& aColors) { + CheckHorizontalInterpolation(aSize, aColors); + CheckVerticalInterpolation(aSize, aColors); +} + +void CheckADAM7InterpolatingWritePixels(const IntSize& aSize) { + // This test writes 8 passes of green pixels (the seven ADAM7 passes, plus one + // extra to make sure nothing goes wrong if we write too much input) and + // verifies that the output is a solid green surface each time. Because all + // the pixels are the same color, interpolation doesn't matter; we test the + // correctness of the interpolation algorithm itself separately. + WithADAM7InterpolatingFilter( + aSize, [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + IntRect rect(IntPoint(0, 0), aSize); + + for (int32_t pass = 1; pass <= 8; ++pass) { + // We only actually write up to the last important row for each pass, + // because that row unambiguously determines the remaining rows. + const int32_t lastRow = aSize.height - 1; + const int32_t lastImportantRow = + lastRow - (lastRow % ImportantRowStride(pass)); + const IntRect inputWriteRect(0, 0, aSize.width, lastImportantRow + 1); + + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(rect), + /* aInputRect = */ Some(rect), + /* aInputWriteRect = */ Some(inputWriteRect)); + + aFilter->ResetToFirstRow(); + EXPECT_FALSE(aFilter->IsSurfaceFinished()); + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + } + }); +} + +TEST(ImageADAM7InterpolatingFilter, WritePixels100_100) +{ CheckADAM7InterpolatingWritePixels(IntSize(100, 100)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels99_99) +{ CheckADAM7InterpolatingWritePixels(IntSize(99, 99)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels66_33) +{ CheckADAM7InterpolatingWritePixels(IntSize(66, 33)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels33_66) +{ CheckADAM7InterpolatingWritePixels(IntSize(33, 66)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels15_15) +{ CheckADAM7InterpolatingWritePixels(IntSize(15, 15)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels9_9) +{ CheckADAM7InterpolatingWritePixels(IntSize(9, 9)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels8_8) +{ CheckADAM7InterpolatingWritePixels(IntSize(8, 8)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels7_7) +{ CheckADAM7InterpolatingWritePixels(IntSize(7, 7)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels3_3) +{ CheckADAM7InterpolatingWritePixels(IntSize(3, 3)); } + +TEST(ImageADAM7InterpolatingFilter, WritePixels1_1) +{ CheckADAM7InterpolatingWritePixels(IntSize(1, 1)); } + +TEST(ImageADAM7InterpolatingFilter, TrivialInterpolation48_48) +{ CheckInterpolation(IntSize(48, 48), {BGRAColor::Green()}); } + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput33_17) +{ + // We check interpolation using irregular patterns to make sure that the + // interpolation will look different for different passes. + CheckInterpolation( + IntSize(33, 17), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput32_16) +{ + CheckInterpolation( + IntSize(32, 16), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput31_15) +{ + CheckInterpolation( + IntSize(31, 15), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(), + BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Blue(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput17_33) +{ + CheckInterpolation(IntSize(17, 33), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), + BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput16_32) +{ + CheckInterpolation(IntSize(16, 32), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), + BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput15_31) +{ + CheckInterpolation(IntSize(15, 31), + {BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(), + BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(), + BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), + BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput9_9) +{ + CheckInterpolation(IntSize(9, 9), + {BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput8_8) +{ + CheckInterpolation(IntSize(8, 8), + {BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput7_7) +{ + CheckInterpolation(IntSize(7, 7), + {BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), + BGRAColor::Green(), BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Red(), BGRAColor::Blue()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput3_3) +{ + CheckInterpolation(IntSize(3, 3), {BGRAColor::Green(), BGRAColor::Red(), + BGRAColor::Blue(), BGRAColor::Red()}); +} + +TEST(ImageADAM7InterpolatingFilter, InterpolationOutput1_1) +{ CheckInterpolation(IntSize(1, 1), {BGRAColor::Blue()}); } + +TEST(ImageADAM7InterpolatingFilter, ADAM7InterpolationFailsFor0_0) +{ + // A 0x0 input size is invalid, so configuration should fail. + AssertConfiguringADAM7InterpolatingFilterFails(IntSize(0, 0)); +} + +TEST(ImageADAM7InterpolatingFilter, ADAM7InterpolationFailsForMinus1_Minus1) +{ + // A negative input size is invalid, so configuration should fail. + AssertConfiguringADAM7InterpolatingFilterFails(IntSize(-1, -1)); +} diff --git a/image/test/gtest/TestAnimationFrameBuffer.cpp b/image/test/gtest/TestAnimationFrameBuffer.cpp new file mode 100644 index 0000000000..78186e5066 --- /dev/null +++ b/image/test/gtest/TestAnimationFrameBuffer.cpp @@ -0,0 +1,895 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include + +#include "AnimationFrameBuffer.h" +#include "Common.h" +#include "gtest/gtest.h" + +using namespace mozilla; +using namespace mozilla::image; + +static already_AddRefed CreateEmptyFrame( + const gfx::IntSize& aSize = gfx::IntSize(1, 1), + const gfx::IntRect& aFrameRect = gfx::IntRect(0, 0, 1, 1), + bool aCanRecycle = true) { + RefPtr frame = new imgFrame(); + AnimationParams animParams{aFrameRect, FrameTimeout::Forever(), + /* aFrameNum */ 1, BlendMethod::OVER, + DisposalMethod::NOT_SPECIFIED}; + nsresult rv = + frame->InitForDecoder(aSize, mozilla::gfx::SurfaceFormat::OS_RGBA, false, + Some(animParams), aCanRecycle); + EXPECT_NS_SUCCEEDED(rv); + RawAccessFrameRef frameRef = frame->RawAccessRef(); + // Normally the blend animation filter would set the dirty rect, but since + // we aren't producing an actual animation here, we need to fake it. + frame->SetDirtyRect(aFrameRect); + frame->Finish(); + return frame.forget(); +} + +static bool ReinitForRecycle(RawAccessFrameRef& aFrame) { + if (!aFrame) { + return false; + } + + AnimationParams animParams{aFrame->GetRect(), FrameTimeout::Forever(), + /* aFrameNum */ 1, BlendMethod::OVER, + DisposalMethod::NOT_SPECIFIED}; + return NS_SUCCEEDED(aFrame->InitForDecoderRecycle(animParams)); +} + +static void PrepareForDiscardingQueue(AnimationFrameRetainedBuffer& aQueue) { + ASSERT_EQ(size_t(0), aQueue.Size()); + ASSERT_LT(size_t(1), aQueue.Batch()); + + AnimationFrameBuffer::InsertStatus status = aQueue.Insert(CreateEmptyFrame()); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + while (true) { + status = aQueue.Insert(CreateEmptyFrame()); + bool restartDecoder = aQueue.AdvanceTo(aQueue.Size() - 1); + EXPECT_FALSE(restartDecoder); + + if (status == AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE) { + break; + } + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + } + + EXPECT_EQ(aQueue.Threshold(), aQueue.Size()); +} + +static void VerifyDiscardingQueueContents( + AnimationFrameDiscardingQueue& aQueue) { + auto frames = aQueue.Display(); + for (auto i : frames) { + EXPECT_TRUE(i != nullptr); + } +} + +static void VerifyInsertInternal(AnimationFrameBuffer& aQueue, + imgFrame* aFrame) { + // Determine the frame index where we just inserted the frame. + size_t frameIndex; + if (aQueue.MayDiscard()) { + const AnimationFrameDiscardingQueue& queue = + *static_cast(&aQueue); + frameIndex = queue.PendingInsert() == 0 ? queue.Size() - 1 + : queue.PendingInsert() - 1; + } else { + ASSERT_FALSE(aQueue.SizeKnown()); + frameIndex = aQueue.Size() - 1; + } + + // Make sure we can get the frame from that index. + RefPtr frame = aQueue.Get(frameIndex, false); + EXPECT_EQ(aFrame, frame.get()); +} + +static void VerifyAdvance(AnimationFrameBuffer& aQueue, size_t aExpectedFrame, + bool aExpectedRestartDecoder) { + RefPtr oldFrame; + size_t totalRecycled; + if (aQueue.IsRecycling()) { + AnimationFrameRecyclingQueue& queue = + *static_cast(&aQueue); + oldFrame = queue.Get(queue.Displayed(), false); + totalRecycled = queue.Recycle().size(); + } + + bool restartDecoder = aQueue.AdvanceTo(aExpectedFrame); + EXPECT_EQ(aExpectedRestartDecoder, restartDecoder); + + if (aQueue.IsRecycling()) { + const AnimationFrameRecyclingQueue& queue = + *static_cast(&aQueue); + EXPECT_FALSE(queue.Recycle().back().mDirtyRect.IsEmpty()); + EXPECT_TRUE( + queue.Recycle().back().mDirtyRect.Contains(oldFrame->GetDirtyRect())); + EXPECT_EQ(totalRecycled + 1, queue.Recycle().size()); + EXPECT_EQ(oldFrame.get(), queue.Recycle().back().mFrame.get()); + } +} + +static void VerifyInsertAndAdvance( + AnimationFrameBuffer& aQueue, size_t aExpectedFrame, + AnimationFrameBuffer::InsertStatus aExpectedStatus) { + // Insert the decoded frame. + RefPtr frame = CreateEmptyFrame(); + AnimationFrameBuffer::InsertStatus status = + aQueue.Insert(RefPtr(frame)); + EXPECT_EQ(aExpectedStatus, status); + EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame)); + VerifyInsertInternal(aQueue, frame); + + // Advance the display frame. + bool expectedRestartDecoder = + aExpectedStatus == AnimationFrameBuffer::InsertStatus::YIELD; + VerifyAdvance(aQueue, aExpectedFrame, expectedRestartDecoder); +} + +static void VerifyMarkComplete( + AnimationFrameBuffer& aQueue, bool aExpectedContinue, + const gfx::IntRect& aRefreshArea = gfx::IntRect(0, 0, 1, 1)) { + if (aQueue.IsRecycling() && !aQueue.SizeKnown()) { + const AnimationFrameRecyclingQueue& queue = + *static_cast(&aQueue); + EXPECT_EQ(queue.FirstFrame()->GetRect(), queue.FirstFrameRefreshArea()); + } + + bool keepDecoding = aQueue.MarkComplete(aRefreshArea); + EXPECT_EQ(aExpectedContinue, keepDecoding); + + if (aQueue.IsRecycling()) { + const AnimationFrameRecyclingQueue& queue = + *static_cast(&aQueue); + EXPECT_EQ(aRefreshArea, queue.FirstFrameRefreshArea()); + } +} + +static void VerifyInsert(AnimationFrameBuffer& aQueue, + AnimationFrameBuffer::InsertStatus aExpectedStatus) { + RefPtr frame = CreateEmptyFrame(); + AnimationFrameBuffer::InsertStatus status = + aQueue.Insert(RefPtr(frame)); + EXPECT_EQ(aExpectedStatus, status); + EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame)); + VerifyInsertInternal(aQueue, frame); +} + +static void VerifyReset(AnimationFrameBuffer& aQueue, bool aExpectedContinue, + const imgFrame* aFirstFrame) { + bool keepDecoding = aQueue.Reset(); + EXPECT_EQ(aExpectedContinue, keepDecoding); + EXPECT_EQ(aQueue.Batch() * 2, aQueue.PendingDecode()); + EXPECT_EQ(aFirstFrame, aQueue.Get(0, true)); + + if (!aQueue.MayDiscard()) { + const AnimationFrameRetainedBuffer& queue = + *static_cast(&aQueue); + EXPECT_EQ(aFirstFrame, queue.Frames()[0].get()); + EXPECT_EQ(aFirstFrame, aQueue.Get(0, false)); + } else { + const AnimationFrameDiscardingQueue& queue = + *static_cast(&aQueue); + EXPECT_EQ(size_t(0), queue.PendingInsert()); + EXPECT_EQ(size_t(0), queue.Display().size()); + EXPECT_EQ(aFirstFrame, queue.FirstFrame()); + EXPECT_EQ(nullptr, aQueue.Get(0, false)); + } +} + +class ImageAnimationFrameBuffer : public ::testing::Test { + public: + ImageAnimationFrameBuffer() {} + + private: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageAnimationFrameBuffer, RetainedInitialState) { + const size_t kThreshold = 800; + const size_t kBatch = 100; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + + EXPECT_EQ(kThreshold, buffer.Threshold()); + EXPECT_EQ(kBatch, buffer.Batch()); + EXPECT_EQ(size_t(0), buffer.Displayed()); + EXPECT_EQ(kBatch * 2, buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); + EXPECT_FALSE(buffer.MayDiscard()); + EXPECT_FALSE(buffer.SizeKnown()); + EXPECT_EQ(size_t(0), buffer.Size()); +} + +TEST_F(ImageAnimationFrameBuffer, ThresholdTooSmall) { + const size_t kThreshold = 0; + const size_t kBatch = 10; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + + EXPECT_EQ(kBatch * 2 + 1, buffer.Threshold()); + EXPECT_EQ(kBatch, buffer.Batch()); + EXPECT_EQ(kBatch * 2, buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); +} + +TEST_F(ImageAnimationFrameBuffer, BatchTooSmall) { + const size_t kThreshold = 10; + const size_t kBatch = 0; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + + EXPECT_EQ(kThreshold, buffer.Threshold()); + EXPECT_EQ(size_t(1), buffer.Batch()); + EXPECT_EQ(size_t(2), buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); +} + +TEST_F(ImageAnimationFrameBuffer, BatchTooBig) { + const size_t kThreshold = 50; + const size_t kBatch = SIZE_MAX; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + + // The rounding is important here (e.g. SIZE_MAX/4 * 2 != SIZE_MAX/2). + EXPECT_EQ(SIZE_MAX / 4, buffer.Batch()); + EXPECT_EQ(buffer.Batch() * 2 + 1, buffer.Threshold()); + EXPECT_EQ(buffer.Batch() * 2, buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); +} + +TEST_F(ImageAnimationFrameBuffer, FinishUnderBatchAndThreshold) { + const size_t kThreshold = 30; + const size_t kBatch = 10; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + const auto& frames = buffer.Frames(); + + EXPECT_EQ(kBatch * 2, buffer.PendingDecode()); + + RefPtr firstFrame; + for (size_t i = 0; i < 5; ++i) { + RefPtr frame = CreateEmptyFrame(); + auto status = buffer.Insert(RefPtr(frame)); + EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE); + EXPECT_FALSE(buffer.SizeKnown()); + EXPECT_EQ(buffer.Size(), i + 1); + + if (i == 4) { + EXPECT_EQ(size_t(15), buffer.PendingDecode()); + bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1)); + EXPECT_FALSE(keepDecoding); + EXPECT_TRUE(buffer.SizeKnown()); + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_FALSE(buffer.HasRedecodeError()); + } + + EXPECT_FALSE(buffer.MayDiscard()); + + imgFrame* gotFrame = buffer.Get(i, false); + EXPECT_EQ(frame.get(), gotFrame); + ASSERT_EQ(i + 1, frames.Length()); + EXPECT_EQ(frame.get(), frames[i].get()); + + if (i == 0) { + firstFrame = std::move(frame); + EXPECT_EQ(size_t(0), buffer.Displayed()); + } else { + EXPECT_EQ(i - 1, buffer.Displayed()); + bool restartDecoder = buffer.AdvanceTo(i); + EXPECT_FALSE(restartDecoder); + EXPECT_EQ(i, buffer.Displayed()); + } + + gotFrame = buffer.Get(0, false); + EXPECT_EQ(firstFrame.get(), gotFrame); + } + + // Loop again over the animation and make sure it is still all there. + for (size_t i = 0; i < frames.Length(); ++i) { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + + bool restartDecoder = buffer.AdvanceTo(i); + EXPECT_FALSE(restartDecoder); + } +} + +TEST_F(ImageAnimationFrameBuffer, FinishMultipleBatchesUnderThreshold) { + const size_t kThreshold = 30; + const size_t kBatch = 2; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0); + const auto& frames = buffer.Frames(); + + EXPECT_EQ(kBatch * 2, buffer.PendingDecode()); + + // Add frames until it tells us to stop. + AnimationFrameBuffer::InsertStatus status; + do { + status = buffer.Insert(CreateEmptyFrame()); + EXPECT_FALSE(buffer.SizeKnown()); + EXPECT_FALSE(buffer.MayDiscard()); + } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE); + + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(4), frames.Length()); + EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::YIELD); + + // Progress through the animation until it lets us decode again. + bool restartDecoder = false; + size_t i = 0; + do { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + if (i > 0) { + restartDecoder = buffer.AdvanceTo(i); + } + ++i; + } while (!restartDecoder); + + EXPECT_EQ(size_t(2), buffer.PendingDecode()); + EXPECT_EQ(size_t(2), buffer.Displayed()); + + // Add the last frame. + status = buffer.Insert(CreateEmptyFrame()); + EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE); + bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1)); + EXPECT_FALSE(keepDecoding); + EXPECT_TRUE(buffer.SizeKnown()); + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(5), frames.Length()); + EXPECT_FALSE(buffer.HasRedecodeError()); + + // Finish progressing through the animation. + for (; i < frames.Length(); ++i) { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + restartDecoder = buffer.AdvanceTo(i); + EXPECT_FALSE(restartDecoder); + } + + // Loop again over the animation and make sure it is still all there. + for (i = 0; i < frames.Length(); ++i) { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + restartDecoder = buffer.AdvanceTo(i); + EXPECT_FALSE(restartDecoder); + } + + // Loop to the third frame and then reset the animation. + for (i = 0; i < 3; ++i) { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + restartDecoder = buffer.AdvanceTo(i); + EXPECT_FALSE(restartDecoder); + } + + // Since we are below the threshold, we can reset the get index only. + // Nothing else should have changed. + restartDecoder = buffer.Reset(); + EXPECT_FALSE(restartDecoder); + for (i = 0; i < 5; ++i) { + EXPECT_TRUE(buffer.Get(i, false) != nullptr); + } + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); + EXPECT_EQ(size_t(0), buffer.Displayed()); +} + +TEST_F(ImageAnimationFrameBuffer, StartAfterBeginning) { + const size_t kThreshold = 30; + const size_t kBatch = 2; + const size_t kStartFrame = 7; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame); + + EXPECT_EQ(kStartFrame, buffer.PendingAdvance()); + + // Add frames until it tells us to stop. It should be later than before, + // because it auto-advances until its displayed frame is kStartFrame. + AnimationFrameBuffer::InsertStatus status; + size_t i = 0; + do { + status = buffer.Insert(CreateEmptyFrame()); + EXPECT_FALSE(buffer.SizeKnown()); + EXPECT_FALSE(buffer.MayDiscard()); + + if (i <= kStartFrame) { + EXPECT_EQ(i, buffer.Displayed()); + EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance()); + } else { + EXPECT_EQ(kStartFrame, buffer.Displayed()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); + } + + i++; + } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE); + + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); + EXPECT_EQ(size_t(10), buffer.Size()); +} + +TEST_F(ImageAnimationFrameBuffer, StartAfterBeginningAndReset) { + const size_t kThreshold = 30; + const size_t kBatch = 2; + const size_t kStartFrame = 7; + AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame); + + EXPECT_EQ(kStartFrame, buffer.PendingAdvance()); + + // Add frames until it tells us to stop. It should be later than before, + // because it auto-advances until its displayed frame is kStartFrame. + for (size_t i = 0; i < 5; ++i) { + AnimationFrameBuffer::InsertStatus status = + buffer.Insert(CreateEmptyFrame()); + EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE); + EXPECT_FALSE(buffer.SizeKnown()); + EXPECT_FALSE(buffer.MayDiscard()); + EXPECT_EQ(i, buffer.Displayed()); + EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance()); + } + + // When we reset the animation, it goes back to the beginning. That means + // we can forget about what we were told to advance to at the start. While + // we have plenty of frames in our buffer, we still need one more because + // in the real scenario, the decoder thread is still running and it is easier + // to let it insert its last frame than to coordinate quitting earlier. + buffer.Reset(); + EXPECT_EQ(size_t(0), buffer.Displayed()); + EXPECT_EQ(size_t(1), buffer.PendingDecode()); + EXPECT_EQ(size_t(0), buffer.PendingAdvance()); + EXPECT_EQ(size_t(5), buffer.Size()); +} + +static void TestDiscardingQueueLoop(AnimationFrameDiscardingQueue& aQueue, + const imgFrame* aFirstFrame, + size_t aThreshold, size_t aBatch, + size_t aStartFrame) { + // We should be advanced right up to the last decoded frame. + EXPECT_TRUE(aQueue.MayDiscard()); + EXPECT_FALSE(aQueue.SizeKnown()); + EXPECT_EQ(aBatch, aQueue.Batch()); + EXPECT_EQ(aThreshold, aQueue.PendingInsert()); + EXPECT_EQ(aThreshold, aQueue.Size()); + EXPECT_EQ(aFirstFrame, aQueue.FirstFrame()); + EXPECT_EQ(size_t(1), aQueue.Display().size()); + EXPECT_EQ(size_t(3), aQueue.PendingDecode()); + VerifyDiscardingQueueContents(aQueue); + + // Make sure frames get removed as we advance. + VerifyInsertAndAdvance(aQueue, 5, + AnimationFrameBuffer::InsertStatus::CONTINUE); + EXPECT_EQ(size_t(1), aQueue.Display().size()); + VerifyInsertAndAdvance(aQueue, 6, + AnimationFrameBuffer::InsertStatus::CONTINUE); + EXPECT_EQ(size_t(1), aQueue.Display().size()); + + // We actually will yield if we are recycling instead of continuing because + // the pending calculation is slightly different. We will actually request one + // less frame than we have to recycle. + if (aQueue.IsRecycling()) { + VerifyInsertAndAdvance(aQueue, 7, + AnimationFrameBuffer::InsertStatus::YIELD); + } else { + VerifyInsertAndAdvance(aQueue, 7, + AnimationFrameBuffer::InsertStatus::CONTINUE); + } + EXPECT_EQ(size_t(1), aQueue.Display().size()); + + // We should get throttled if we insert too much. + VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE); + EXPECT_EQ(size_t(2), aQueue.Display().size()); + EXPECT_EQ(size_t(1), aQueue.PendingDecode()); + VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD); + EXPECT_EQ(size_t(3), aQueue.Display().size()); + EXPECT_EQ(size_t(0), aQueue.PendingDecode()); + + // We should get restarted if we advance. + VerifyAdvance(aQueue, 8, true); + EXPECT_EQ(size_t(2), aQueue.PendingDecode()); + VerifyAdvance(aQueue, 9, false); + EXPECT_EQ(size_t(2), aQueue.PendingDecode()); + + // We should continue decoding if we completed, since we are discarding. + VerifyMarkComplete(aQueue, true); + EXPECT_EQ(size_t(2), aQueue.PendingDecode()); + EXPECT_EQ(size_t(10), aQueue.Size()); + EXPECT_TRUE(aQueue.SizeKnown()); + EXPECT_FALSE(aQueue.HasRedecodeError()); + + // Insert the first frames of the animation. + VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD); + EXPECT_EQ(size_t(0), aQueue.PendingDecode()); + EXPECT_EQ(size_t(10), aQueue.Size()); + + // Advance back at the beginning. The first frame should only match for + // display purposes. + VerifyAdvance(aQueue, 0, true); + EXPECT_EQ(size_t(2), aQueue.PendingDecode()); + EXPECT_TRUE(aQueue.FirstFrame() != nullptr); + EXPECT_TRUE(aQueue.Get(0, false) != nullptr); + EXPECT_NE(aQueue.FirstFrame(), aQueue.Get(0, false)); + EXPECT_EQ(aQueue.FirstFrame(), aQueue.Get(0, true)); + + // Reiterate one more time and make it loops back. + VerifyInsertAndAdvance(aQueue, 1, + AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(aQueue, 2, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(aQueue, 3, + AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(aQueue, 4, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(aQueue, 5, + AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(aQueue, 6, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(aQueue, 7, + AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(aQueue, 8, AnimationFrameBuffer::InsertStatus::YIELD); + + EXPECT_EQ(size_t(10), aQueue.PendingInsert()); + VerifyMarkComplete(aQueue, true); + EXPECT_EQ(size_t(0), aQueue.PendingInsert()); + + VerifyInsertAndAdvance(aQueue, 9, + AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(aQueue, 0, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(aQueue, 1, + AnimationFrameBuffer::InsertStatus::CONTINUE); +} + +TEST_F(ImageAnimationFrameBuffer, DiscardingLoop) { + const size_t kThreshold = 5; + const size_t kBatch = 2; + const size_t kStartFrame = 0; + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + PrepareForDiscardingQueue(retained); + const imgFrame* firstFrame = retained.Frames()[0].get(); + AnimationFrameDiscardingQueue buffer(std::move(retained)); + TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame); +} + +TEST_F(ImageAnimationFrameBuffer, RecyclingLoop) { + const size_t kThreshold = 5; + const size_t kBatch = 2; + const size_t kStartFrame = 0; + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + PrepareForDiscardingQueue(retained); + const imgFrame* firstFrame = retained.Frames()[0].get(); + AnimationFrameRecyclingQueue buffer(std::move(retained)); + + // We should not start with any recycled frames. + ASSERT_TRUE(buffer.Recycle().empty()); + + TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame); + + // All the frames we inserted should have been recycleable. + ASSERT_FALSE(buffer.Recycle().empty()); + while (!buffer.Recycle().empty()) { + gfx::IntRect expectedRect(0, 0, 1, 1); + RefPtr expectedFrame = buffer.Recycle().front().mFrame; + EXPECT_FALSE(expectedRect.IsEmpty()); + EXPECT_TRUE(expectedFrame.get() != nullptr); + + gfx::IntRect gotRect; + RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect); + EXPECT_EQ(expectedFrame.get(), gotFrame.get()); + EXPECT_EQ(expectedRect, gotRect); + EXPECT_TRUE(ReinitForRecycle(gotFrame)); + } + + // Trying to pull a recycled frame when we have nothing should be safe too. + gfx::IntRect gotRect; + RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect); + EXPECT_TRUE(gotFrame.get() == nullptr); + EXPECT_FALSE(ReinitForRecycle(gotFrame)); +} + +static void TestDiscardingQueueReset(AnimationFrameDiscardingQueue& aQueue, + const imgFrame* aFirstFrame, + size_t aThreshold, size_t aBatch, + size_t aStartFrame) { + // We should be advanced right up to the last decoded frame. + EXPECT_TRUE(aQueue.MayDiscard()); + EXPECT_FALSE(aQueue.SizeKnown()); + EXPECT_EQ(aBatch, aQueue.Batch()); + EXPECT_EQ(aThreshold, aQueue.PendingInsert()); + EXPECT_EQ(aThreshold, aQueue.Size()); + EXPECT_EQ(aFirstFrame, aQueue.FirstFrame()); + EXPECT_EQ(size_t(1), aQueue.Display().size()); + EXPECT_EQ(size_t(4), aQueue.PendingDecode()); + VerifyDiscardingQueueContents(aQueue); + + // Reset should clear everything except the first frame. + VerifyReset(aQueue, false, aFirstFrame); +} + +TEST_F(ImageAnimationFrameBuffer, DiscardingReset) { + const size_t kThreshold = 8; + const size_t kBatch = 3; + const size_t kStartFrame = 0; + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + PrepareForDiscardingQueue(retained); + const imgFrame* firstFrame = retained.Frames()[0].get(); + AnimationFrameDiscardingQueue buffer(std::move(retained)); + TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame); +} + +TEST_F(ImageAnimationFrameBuffer, ResetBeforeDiscardingThreshold) { + const size_t kThreshold = 3; + const size_t kBatch = 1; + const size_t kStartFrame = 0; + + // Get the starting buffer to just before the point where we need to switch + // to a discarding buffer, reset the animation so advancing points at the + // first frame, and insert the last frame to cross the threshold. + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(retained, 1, + AnimationFrameBuffer::InsertStatus::YIELD); + bool restartDecoder = retained.Reset(); + EXPECT_FALSE(restartDecoder); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD); + + const imgFrame* firstFrame = retained.Frames()[0].get(); + EXPECT_TRUE(firstFrame != nullptr); + AnimationFrameDiscardingQueue buffer(std::move(retained)); + const imgFrame* displayFirstFrame = buffer.Get(0, true); + const imgFrame* advanceFirstFrame = buffer.Get(0, false); + EXPECT_EQ(firstFrame, displayFirstFrame); + EXPECT_EQ(firstFrame, advanceFirstFrame); +} + +TEST_F(ImageAnimationFrameBuffer, DiscardingTooFewFrames) { + const size_t kThreshold = 3; + const size_t kBatch = 1; + const size_t kStartFrame = 0; + + // First get us to a discarding buffer state. + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(retained, 1, + AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD); + + // Insert one more frame. + AnimationFrameDiscardingQueue buffer(std::move(retained)); + VerifyAdvance(buffer, 2, true); + VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD); + + // Mark it as complete. + bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1)); + EXPECT_FALSE(restartDecoder); + EXPECT_FALSE(buffer.HasRedecodeError()); + + // Insert one fewer frame than before. + VerifyAdvance(buffer, 3, true); + VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD); + + // When we mark it as complete, it should fail due to too few frames. + restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1)); + EXPECT_TRUE(buffer.HasRedecodeError()); + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(4), buffer.Size()); +} + +TEST_F(ImageAnimationFrameBuffer, DiscardingTooManyFrames) { + const size_t kThreshold = 3; + const size_t kBatch = 1; + const size_t kStartFrame = 0; + + // First get us to a discarding buffer state. + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE); + VerifyInsertAndAdvance(retained, 1, + AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD); + + // Insert one more frame. + AnimationFrameDiscardingQueue buffer(std::move(retained)); + VerifyAdvance(buffer, 2, true); + VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD); + + // Mark it as complete. + bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1)); + EXPECT_FALSE(restartDecoder); + EXPECT_FALSE(buffer.HasRedecodeError()); + + // Advance and insert to get us back to the end on the redecode. + VerifyAdvance(buffer, 3, true); + VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD); + VerifyInsertAndAdvance(buffer, 3, AnimationFrameBuffer::InsertStatus::YIELD); + + // Attempt to insert a 5th frame, it should fail. + RefPtr frame = CreateEmptyFrame(); + AnimationFrameBuffer::InsertStatus status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + EXPECT_TRUE(buffer.HasRedecodeError()); + EXPECT_EQ(size_t(0), buffer.PendingDecode()); + EXPECT_EQ(size_t(4), buffer.Size()); +} + +TEST_F(ImageAnimationFrameBuffer, RecyclingReset) { + const size_t kThreshold = 8; + const size_t kBatch = 3; + const size_t kStartFrame = 0; + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + PrepareForDiscardingQueue(retained); + const imgFrame* firstFrame = retained.Frames()[0].get(); + AnimationFrameRecyclingQueue buffer(std::move(retained)); + TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame); +} + +TEST_F(ImageAnimationFrameBuffer, RecyclingResetBeforeComplete) { + const size_t kThreshold = 3; + const size_t kBatch = 1; + const size_t kStartFrame = 0; + const gfx::IntSize kImageSize(100, 100); + const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize); + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + + // Get the starting buffer to just before the point where we need to switch + // to a discarding buffer, reset the animation so advancing points at the + // first frame, and insert the last frame to cross the threshold. + RefPtr frame; + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + frame = CreateEmptyFrame( + kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)), + false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + VerifyAdvance(retained, 1, true); + + frame = CreateEmptyFrame( + kImageSize, gfx::IntRect(gfx::IntPoint(20, 10), gfx::IntSize(1, 1)), + false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_YIELD, status); + + AnimationFrameRecyclingQueue buffer(std::move(retained)); + bool restartDecoding = buffer.Reset(); + EXPECT_TRUE(restartDecoding); + + // None of the buffers were recyclable. + EXPECT_FALSE(buffer.Recycle().empty()); + while (!buffer.Recycle().empty()) { + gfx::IntRect recycleRect; + RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_FALSE(ReinitForRecycle(frameRef)); + } + + // Reinsert the first two frames as recyclable and reset again. + frame = CreateEmptyFrame(kImageSize, kImageRect, true); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + frame = CreateEmptyFrame( + kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)), + true); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + restartDecoding = buffer.Reset(); + EXPECT_TRUE(restartDecoding); + + // Now both buffers should have been saved and the dirty rect replaced with + // the full image rect since we don't know the first frame refresh area yet. + EXPECT_EQ(size_t(2), buffer.Recycle().size()); + for (const auto& entry : buffer.Recycle()) { + EXPECT_EQ(kImageRect, entry.mDirtyRect); + } +} + +TEST_F(ImageAnimationFrameBuffer, RecyclingRect) { + const size_t kThreshold = 5; + const size_t kBatch = 2; + const size_t kStartFrame = 0; + const gfx::IntSize kImageSize(100, 100); + const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize); + AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame); + + // Let's get to the recycling state while marking all of the frames as not + // recyclable, just like AnimationFrameBuffer / the decoders would do. + RefPtr frame; + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status); + + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + VerifyAdvance(retained, 1, false); + VerifyAdvance(retained, 2, true); + VerifyAdvance(retained, 3, false); + + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + status = retained.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE, status); + + AnimationFrameRecyclingQueue buffer(std::move(retained)); + + // The first frame is now the candidate for recycling. Since it was marked as + // not recyclable, we should get nothing. + VerifyAdvance(buffer, 4, false); + + gfx::IntRect recycleRect; + EXPECT_FALSE(buffer.Recycle().empty()); + RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_FALSE(ReinitForRecycle(frameRef)); + EXPECT_TRUE(buffer.Recycle().empty()); + + // Insert a recyclable partial frame. Its dirty rect shouldn't matter since + // the previous frame was not recyclable. + frame = CreateEmptyFrame(kImageSize, gfx::IntRect(0, 0, 25, 25)); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + VerifyAdvance(buffer, 5, true); + EXPECT_FALSE(buffer.Recycle().empty()); + frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_FALSE(ReinitForRecycle(frameRef)); + EXPECT_TRUE(buffer.Recycle().empty()); + + // Insert a recyclable partial frame. Its dirty rect should match the recycle + // rect since it is the only frame in the buffer. + frame = CreateEmptyFrame(kImageSize, gfx::IntRect(25, 0, 50, 50)); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + VerifyAdvance(buffer, 6, true); + EXPECT_FALSE(buffer.Recycle().empty()); + frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_TRUE(ReinitForRecycle(frameRef)); + EXPECT_EQ(gfx::IntRect(25, 0, 50, 50), recycleRect); + EXPECT_TRUE(buffer.Recycle().empty()); + + // Insert the last frame and mark us as complete. The next recycled frame is + // producing the first frame again, so we should use the first frame refresh + // area instead of its dirty rect. + frame = CreateEmptyFrame(kImageSize, gfx::IntRect(10, 10, 60, 10)); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + bool continueDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 75, 50)); + EXPECT_FALSE(continueDecoding); + + VerifyAdvance(buffer, 7, true); + EXPECT_FALSE(buffer.Recycle().empty()); + frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_TRUE(ReinitForRecycle(frameRef)); + EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect); + EXPECT_TRUE(buffer.Recycle().empty()); + + // Now let's reinsert the first frame. The recycle rect should still be the + // first frame refresh area instead of the dirty rect of the first frame (e.g. + // the full frame). + frame = CreateEmptyFrame(kImageSize, kImageRect, false); + status = buffer.Insert(std::move(frame)); + EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status); + + VerifyAdvance(buffer, 0, true); + EXPECT_FALSE(buffer.Recycle().empty()); + frameRef = buffer.RecycleFrame(recycleRect); + EXPECT_TRUE(frameRef); + EXPECT_TRUE(ReinitForRecycle(frameRef)); + EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect); + EXPECT_TRUE(buffer.Recycle().empty()); +} diff --git a/image/test/gtest/TestBlendAnimationFilter.cpp b/image/test/gtest/TestBlendAnimationFilter.cpp new file mode 100644 index 0000000000..7291fbc3f6 --- /dev/null +++ b/image/test/gtest/TestBlendAnimationFilter.cpp @@ -0,0 +1,450 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "skia/include/core/SkColorPriv.h" // for SkPMSrcOver +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +static already_AddRefed CreateTrivialBlendingDecoder() { + DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif"); + DecoderFlags decoderFlags = DefaultDecoderFlags(); + SurfaceFlags surfaceFlags = DefaultSurfaceFlags(); + auto sourceBuffer = MakeNotNull>(); + return DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, Nothing(), decoderFlags, surfaceFlags); +} + +template +RawAccessFrameRef WithBlendAnimationFilter(image::Decoder* aDecoder, + const AnimationParams& aAnimParams, + const IntSize& aOutputSize, + Func aFunc) { + DecoderTestHelper decoderHelper(aDecoder); + + if (!aDecoder->HasAnimation()) { + decoderHelper.PostIsAnimated(aAnimParams.mTimeout); + } + + BlendAnimationConfig blendAnim{aDecoder}; + SurfaceConfig surfaceSink{aDecoder, aOutputSize, SurfaceFormat::OS_RGBA, + false, Some(aAnimParams)}; + + auto func = [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + aFunc(aDecoder, aFilter); + }; + + WithFilterPipeline(aDecoder, func, false, blendAnim, surfaceSink); + + RawAccessFrameRef current = aDecoder->GetCurrentFrameRef(); + if (current) { + decoderHelper.PostFrameStop(Opacity::SOME_TRANSPARENCY); + } + + return current; +} + +void AssertConfiguringBlendAnimationFilterFails(const IntRect& aFrameRect, + const IntSize& aOutputSize) { + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams animParams{aFrameRect, FrameTimeout::FromRawMilliseconds(0), + 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BlendAnimationConfig blendAnim{decoder}; + SurfaceConfig surfaceSink{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false, + Some(animParams)}; + AssertConfiguringPipelineFails(decoder, blendAnim, surfaceSink); +} + +TEST(ImageBlendAnimationFilter, BlendFailsForNegativeFrameRect) +{ + // A negative frame rect size is disallowed. + AssertConfiguringBlendAnimationFilterFails( + IntRect(IntPoint(0, 0), IntSize(-1, -1)), IntSize(100, 100)); +} + +TEST(ImageBlendAnimationFilter, WriteFullFirstFrame) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100))); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); +} + +TEST(ImageBlendAnimationFilter, WritePartialFirstFrame) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params{ + IntRect(25, 50, 50, 25), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)), + Nothing(), Some(IntRect(25, 50, 50, 25)), + Some(IntRect(25, 50, 50, 25))); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); +} + +static void TestWithBlendAnimationFilterClear(BlendMethod aBlendMethod) { + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(BGRAColor::Green().AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::CLEAR}; + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(BGRAColor::Red().AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + RefPtr surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red())); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green())); + + AnimationParams params2{ + IntRect(0, 50, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, aBlendMethod, DisposalMethod::KEEP}; + RawAccessFrameRef frame2 = WithBlendAnimationFilter( + decoder, params2, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(BGRAColor::Blue().AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + + ASSERT_TRUE(frame2.get() != nullptr); + + surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 10, BGRAColor::Transparent())); + EXPECT_TRUE(RowsAreSolidColor(surface, 50, 20, BGRAColor::Blue())); + EXPECT_TRUE(RowsAreSolidColor(surface, 70, 30, BGRAColor::Green())); +} + +TEST(ImageBlendAnimationFilter, ClearWithOver) +{ TestWithBlendAnimationFilterClear(BlendMethod::OVER); } + +TEST(ImageBlendAnimationFilter, ClearWithSource) +{ TestWithBlendAnimationFilterClear(BlendMethod::SOURCE); } + +TEST(ImageBlendAnimationFilter, KeepWithSource) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(BGRAColor::Green().AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::KEEP}; + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(BGRAColor::Red().AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + RefPtr surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red())); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green())); +} + +TEST(ImageBlendAnimationFilter, KeepWithOver) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor0.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::OVER, DisposalMethod::KEEP}; + BGRAColor frameColor1(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor1.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied + EXPECT_EQ(SkPMSrcOver(frameColor1.AsPixel(), frameColor0.AsPixel()), + blendedColor.AsPixel()); + + RefPtr surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousWithOver) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor0.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS}; + BGRAColor frameColor1 = BGRAColor::Green(); + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor1.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect()); + + AnimationParams params2{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::OVER, DisposalMethod::KEEP}; + BGRAColor frameColor2(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame2 = WithBlendAnimationFilter( + decoder, params2, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor2.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect()); + + ASSERT_TRUE(frame2.get() != nullptr); + + BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied + EXPECT_EQ(SkPMSrcOver(frameColor2.AsPixel(), frameColor0.AsPixel()), + blendedColor.AsPixel()); + + RefPtr surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousWithSource) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor0.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS}; + BGRAColor frameColor1 = BGRAColor::Green(); + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor1.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect()); + + AnimationParams params2{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor2(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame2 = WithBlendAnimationFilter( + decoder, params2, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor2.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect()); + + ASSERT_TRUE(frame2.get() != nullptr); + + RefPtr surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor2)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousClearWithSource) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor0 = BGRAColor::Red(); + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor0.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1{ + IntRect(0, 0, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::CLEAR}; + BGRAColor frameColor1 = BGRAColor::Blue(); + RawAccessFrameRef frame1 = WithBlendAnimationFilter( + decoder, params1, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor1.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 20), frame1->GetDirtyRect()); + + AnimationParams params2{ + IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS}; + BGRAColor frameColor2 = BGRAColor::Green(); + RawAccessFrameRef frame2 = WithBlendAnimationFilter( + decoder, params2, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor2.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 90), frame2->GetDirtyRect()); + + AnimationParams params3{ + IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 3, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor3 = BGRAColor::Blue(); + RawAccessFrameRef frame3 = WithBlendAnimationFilter( + decoder, params3, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor3.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 90), frame3->GetDirtyRect()); + + ASSERT_TRUE(frame3.get() != nullptr); + + RefPtr surface = frame3->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Transparent())); + EXPECT_TRUE(RowsAreSolidColor(surface, 20, 20, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, PartialOverlapFrameRect) +{ + RefPtr decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0{ + IntRect(-10, -20, 110, 100), FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP}; + BGRAColor frameColor0 = BGRAColor::Red(); + RawAccessFrameRef frame0 = WithBlendAnimationFilter( + decoder, params0, IntSize(100, 100), + [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels( + [&] { return AsVariant(frameColor0.AsPixel()); }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + RefPtr surface = frame0->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 80, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Transparent())); +} diff --git a/image/test/gtest/TestCopyOnWrite.cpp b/image/test/gtest/TestCopyOnWrite.cpp new file mode 100644 index 0000000000..d5ad3e4f6b --- /dev/null +++ b/image/test/gtest/TestCopyOnWrite.cpp @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "CopyOnWrite.h" + +using namespace mozilla; +using namespace mozilla::image; + +struct ValueStats { + int32_t mCopies = 0; + int32_t mFrees = 0; + int32_t mCalls = 0; + int32_t mConstCalls = 0; + int32_t mSerial = 0; +}; + +struct Value { + NS_INLINE_DECL_REFCOUNTING(Value) + + explicit Value(ValueStats& aStats) + : mStats(aStats), mSerial(mStats.mSerial++) {} + + Value(const Value& aOther) + : mStats(aOther.mStats), mSerial(mStats.mSerial++) { + mStats.mCopies++; + } + + void Go() { mStats.mCalls++; } + void Go() const { mStats.mConstCalls++; } + + int32_t Serial() const { return mSerial; } + + protected: + ~Value() { mStats.mFrees++; } + + private: + ValueStats& mStats; + int32_t mSerial; +}; + +TEST(ImageCopyOnWrite, Read) +{ + ValueStats stats; + + { + CopyOnWrite cow(new Value(stats)); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_TRUE(cow.CanRead()); + + cow.Read([&](const Value* aValue) { + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, aValue->Serial()); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + aValue->Go(); + + EXPECT_EQ(0, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + }); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + } + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(1, stats.mFrees); +} + +TEST(ImageCopyOnWrite, RecursiveRead) +{ + ValueStats stats; + + { + CopyOnWrite cow(new Value(stats)); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_TRUE(cow.CanRead()); + + cow.Read([&](const Value* aValue) { + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, aValue->Serial()); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + // Make sure that Read() inside a Read() succeeds. + cow.Read( + [&](const Value* aValue) { + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, aValue->Serial()); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + aValue->Go(); + + EXPECT_EQ(0, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + }, + []() { + // This gets called if we can't read. We shouldn't get here. + EXPECT_TRUE(false); + }); + }); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + } + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(1, stats.mFrees); +} + +TEST(ImageCopyOnWrite, Write) +{ + ValueStats stats; + + { + CopyOnWrite cow(new Value(stats)); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + cow.Write([&](Value* aValue) { + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, aValue->Serial()); + EXPECT_TRUE(!cow.CanRead()); + EXPECT_TRUE(!cow.CanWrite()); + + aValue->Go(); + + EXPECT_EQ(1, stats.mCalls); + EXPECT_EQ(0, stats.mConstCalls); + }); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(1, stats.mCalls); + EXPECT_EQ(0, stats.mConstCalls); + } + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(1, stats.mFrees); +} + +TEST(ImageCopyOnWrite, WriteRecursive) +{ + ValueStats stats; + + { + CopyOnWrite cow(new Value(stats)); + + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + cow.Read([&](const Value* aValue) { + EXPECT_EQ(0, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(0, aValue->Serial()); + EXPECT_TRUE(cow.CanRead()); + EXPECT_TRUE(cow.CanWrite()); + + // Make sure Write() inside a Read() succeeds. + cow.Write( + [&](Value* aValue) { + EXPECT_EQ(1, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(1, aValue->Serial()); + EXPECT_TRUE(!cow.CanRead()); + EXPECT_TRUE(!cow.CanWrite()); + + aValue->Go(); + + EXPECT_EQ(1, stats.mCalls); + EXPECT_EQ(0, stats.mConstCalls); + + // Make sure Read() inside a Write() fails. + cow.Read( + [](const Value* aValue) { + // This gets called if we can read. We shouldn't get here. + EXPECT_TRUE(false); + }, + []() { + // This gets called if we can't read. We *should* get here. + EXPECT_TRUE(true); + }); + + // Make sure Write() inside a Write() fails. + cow.Write( + [](Value* aValue) { + // This gets called if we can write. We shouldn't get here. + EXPECT_TRUE(false); + }, + []() { + // This gets called if we can't write. We *should* get here. + EXPECT_TRUE(true); + }); + }, + []() { + // This gets called if we can't write. We shouldn't get here. + EXPECT_TRUE(false); + }); + + aValue->Go(); + + EXPECT_EQ(1, stats.mCopies); + EXPECT_EQ(0, stats.mFrees); + EXPECT_EQ(1, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + }); + + EXPECT_EQ(1, stats.mCopies); + EXPECT_EQ(1, stats.mFrees); + EXPECT_EQ(1, stats.mCalls); + EXPECT_EQ(1, stats.mConstCalls); + } + + EXPECT_EQ(1, stats.mCopies); + EXPECT_EQ(2, stats.mFrees); +} diff --git a/image/test/gtest/TestDecodeToSurface.cpp b/image/test/gtest/TestDecodeToSurface.cpp new file mode 100644 index 0000000000..838f957c43 --- /dev/null +++ b/image/test/gtest/TestDecodeToSurface.cpp @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "imgIContainer.h" +#include "ImageOps.h" +#include "mozilla/gfx/2D.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "mozilla/RefPtr.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +class DecodeToSurfaceRunnable : public Runnable { + public: + DecodeToSurfaceRunnable(RefPtr& aSurface, + nsIInputStream* aInputStream, + ImageOps::ImageBuffer* aImageBuffer, + const ImageTestCase& aTestCase) + : mozilla::Runnable("DecodeToSurfaceRunnable"), + mSurface(aSurface), + mInputStream(aInputStream), + mImageBuffer(aImageBuffer), + mTestCase(aTestCase) {} + + NS_IMETHOD Run() override { + Go(); + return NS_OK; + } + + void Go() { + Maybe outputSize; + if (mTestCase.mOutputSize != mTestCase.mSize) { + outputSize.emplace(mTestCase.mOutputSize); + } + + uint32_t flags = FromSurfaceFlags(mTestCase.mSurfaceFlags); + + if (mImageBuffer) { + mSurface = ImageOps::DecodeToSurface( + mImageBuffer, nsDependentCString(mTestCase.mMimeType), flags, + outputSize); + } else { + mSurface = ImageOps::DecodeToSurface( + mInputStream.forget(), nsDependentCString(mTestCase.mMimeType), flags, + outputSize); + } + ASSERT_TRUE(mSurface != nullptr); + + EXPECT_TRUE(mSurface->IsDataSourceSurface()); + EXPECT_TRUE(mSurface->GetFormat() == SurfaceFormat::OS_RGBX || + mSurface->GetFormat() == SurfaceFormat::OS_RGBA); + + if (outputSize) { + EXPECT_EQ(*outputSize, mSurface->GetSize()); + } else { + EXPECT_EQ(mTestCase.mSize, mSurface->GetSize()); + } + + EXPECT_TRUE(IsSolidColor(mSurface, mTestCase.Color(), mTestCase.Fuzz())); + } + + private: + RefPtr& mSurface; + nsCOMPtr mInputStream; + RefPtr mImageBuffer; + ImageTestCase mTestCase; +}; + +static void RunDecodeToSurface(const ImageTestCase& aTestCase, + ImageOps::ImageBuffer* aImageBuffer = nullptr) { + nsCOMPtr inputStream; + if (!aImageBuffer) { + inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + } + + nsCOMPtr thread; + nsresult rv = + NS_NewNamedThread("DecodeToSurface", getter_AddRefs(thread), nullptr); + ASSERT_NS_SUCCEEDED(rv); + + // We run the DecodeToSurface tests off-main-thread to ensure that + // DecodeToSurface doesn't require any main-thread-only code. + RefPtr surface; + nsCOMPtr runnable = new DecodeToSurfaceRunnable( + surface, inputStream, aImageBuffer, aTestCase); + NS_DispatchAndSpinEventLoopUntilComplete("RunDecodeToSurface"_ns, thread, + do_AddRef(runnable)); + + thread->Shutdown(); + + // Explicitly release the SourceSurface on the main thread. + surface = nullptr; +} + +class ImageDecodeToSurface : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageDecodeToSurface, PNG) { RunDecodeToSurface(GreenPNGTestCase()); } +TEST_F(ImageDecodeToSurface, GIF) { RunDecodeToSurface(GreenGIFTestCase()); } +TEST_F(ImageDecodeToSurface, JPG) { RunDecodeToSurface(GreenJPGTestCase()); } +TEST_F(ImageDecodeToSurface, BMP) { RunDecodeToSurface(GreenBMPTestCase()); } +TEST_F(ImageDecodeToSurface, ICO) { RunDecodeToSurface(GreenICOTestCase()); } +TEST_F(ImageDecodeToSurface, Icon) { RunDecodeToSurface(GreenIconTestCase()); } +TEST_F(ImageDecodeToSurface, WebP) { RunDecodeToSurface(GreenWebPTestCase()); } + +TEST_F(ImageDecodeToSurface, AnimatedGIF) { + RunDecodeToSurface(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecodeToSurface, AnimatedPNG) { + RunDecodeToSurface(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST_F(ImageDecodeToSurface, Corrupt) { + ImageTestCase testCase = CorruptTestCase(); + + nsCOMPtr inputStream = LoadFile(testCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + RefPtr surface = ImageOps::DecodeToSurface( + inputStream.forget(), nsDependentCString(testCase.mMimeType), + imgIContainer::DECODE_FLAGS_DEFAULT); + EXPECT_TRUE(surface == nullptr); +} + +TEST_F(ImageDecodeToSurface, ICOMultipleSizes) { + ImageTestCase testCase = GreenMultipleSizesICOTestCase(); + + nsCOMPtr inputStream = LoadFile(testCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + RefPtr buffer = + ImageOps::CreateImageBuffer(inputStream.forget()); + ASSERT_TRUE(buffer != nullptr); + + ImageMetadata metadata; + nsresult rv = ImageOps::DecodeMetadata( + buffer, nsDependentCString(testCase.mMimeType), metadata); + EXPECT_NS_SUCCEEDED(rv); + ASSERT_TRUE(metadata.HasSize()); + EXPECT_EQ(testCase.mSize, metadata.GetSize().ToUnknownSize()); + + const nsTArray& nativeSizes = metadata.GetNativeSizes(); + ASSERT_EQ(6u, nativeSizes.Length()); + + OrientedIntSize expectedSizes[] = { + OrientedIntSize(16, 16), OrientedIntSize(32, 32), + OrientedIntSize(64, 64), OrientedIntSize(128, 128), + OrientedIntSize(256, 256), OrientedIntSize(256, 128), + }; + + for (int i = 0; i < 6; ++i) { + EXPECT_EQ(expectedSizes[i], nativeSizes[i]); + + // Request decoding at native size + testCase.mOutputSize = nativeSizes[i].ToUnknownSize(); + RunDecodeToSurface(testCase, buffer); + } +} diff --git a/image/test/gtest/TestDecoders.cpp b/image/test/gtest/TestDecoders.cpp new file mode 100644 index 0000000000..f043b011d0 --- /dev/null +++ b/image/test/gtest/TestDecoders.cpp @@ -0,0 +1,1142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "AnimationSurfaceProvider.h" +#include "DecodePool.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "decoders/nsBMPDecoder.h" +#include "IDecodingTask.h" +#include "ImageOps.h" +#include "imgIContainer.h" +#include "ImageFactory.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/gfx/2D.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "mozilla/RefPtr.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "ProgressTracker.h" +#include "SourceBuffer.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +static already_AddRefed CheckDecoderState( + const ImageTestCase& aTestCase, image::Decoder* aDecoder) { + // image::Decoder should match what we asked for in the MIME type. + EXPECT_NE(aDecoder->GetType(), DecoderType::UNKNOWN); + EXPECT_EQ(aDecoder->GetType(), + DecoderFactory::GetDecoderType(aTestCase.mMimeType)); + + EXPECT_TRUE(aDecoder->GetDecodeDone()); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), aDecoder->HasError()); + + // Verify that the decoder made the expected progress. + Progress progress = aDecoder->TakeProgress(); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), + bool(progress & FLAG_HAS_ERROR)); + + if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) { + return nullptr; // That's all we can check for bad images. + } + + EXPECT_TRUE(bool(progress & FLAG_SIZE_AVAILABLE)); + EXPECT_TRUE(bool(progress & FLAG_DECODE_COMPLETE)); + EXPECT_TRUE(bool(progress & FLAG_FRAME_COMPLETE)); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_TRANSPARENT), + bool(progress & FLAG_HAS_TRANSPARENCY)); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED), + bool(progress & FLAG_IS_ANIMATED)); + + // The decoder should get the correct size. + OrientedIntSize size = aDecoder->Size(); + EXPECT_EQ(aTestCase.mSize.width, size.width); + EXPECT_EQ(aTestCase.mSize.height, size.height); + + // Get the current frame, which is always the first frame of the image + // because CreateAnonymousDecoder() forces a first-frame-only decode. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + // Verify that the resulting surfaces matches our expectations. + EXPECT_TRUE(surface->IsDataSourceSurface()); + EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX || + surface->GetFormat() == SurfaceFormat::OS_RGBA); + EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize()); + + return surface.forget(); +} + +static void CheckDecoderResults(const ImageTestCase& aTestCase, + image::Decoder* aDecoder) { + RefPtr surface = CheckDecoderState(aTestCase, aDecoder); + if (!surface) { + return; + } + + if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) { + return; + } + + // Check the output. + EXPECT_TRUE(IsSolidColor(surface, aTestCase.Color(), aTestCase.Fuzz())); +} + +template +void WithBadBufferDecode(const ImageTestCase& aTestCase, + const Maybe& aOutputSize, + Func aResultChecker) { + // Prepare a SourceBuffer with an error that will immediately move iterators + // to COMPLETE. + auto sourceBuffer = MakeNotNull>(); + sourceBuffer->ExpectLength(SIZE_MAX); + + // Create a decoder. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + + // Run the full decoder synchronously on the main thread. + task->Run(); + + // Call the lambda to verify the expected results. + aResultChecker(decoder); +} + +static void CheckDecoderBadBuffer(const ImageTestCase& aTestCase) { + WithBadBufferDecode(aTestCase, Nothing(), [&](image::Decoder* aDecoder) { + CheckDecoderResults(aTestCase, aDecoder); + }); +} + +template +void WithSingleChunkDecode(const ImageTestCase& aTestCase, + const Maybe& aOutputSize, + bool aUseDecodePool, Func aResultChecker) { + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into a SourceBuffer. + auto sourceBuffer = MakeNotNull>(); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + sourceBuffer->Complete(NS_OK); + + // Create a decoder. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + DecoderFlags decoderFlags = + DecoderFactory::GetDefaultDecoderFlagsForType(decoderType) | + DecoderFlags::FIRST_FRAME_ONLY; + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, aOutputSize, decoderFlags, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + + if (aUseDecodePool) { + DecodePool::Singleton()->AsyncRun(task.get()); + + while (!decoder->GetDecodeDone()) { + task->Resume(); + } + } else { // Run the full decoder synchronously on the main thread. + task->Run(); + } + + // Call the lambda to verify the expected results. + aResultChecker(decoder); +} + +static void CheckDecoderSingleChunk(const ImageTestCase& aTestCase, + bool aUseDecodePool = false) { + WithSingleChunkDecode(aTestCase, Nothing(), aUseDecodePool, + [&](image::Decoder* aDecoder) { + CheckDecoderResults(aTestCase, aDecoder); + }); +} + +template +void WithDelayedChunkDecode(const ImageTestCase& aTestCase, + const Maybe& aOutputSize, + Func aResultChecker) { + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Prepare an empty SourceBuffer. + auto sourceBuffer = MakeNotNull>(); + + // Create a decoder. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ true); + + // Run the full decoder synchronously. It should now be waiting on + // the iterator to yield some data since we haven't written anything yet. + task->Run(); + + // Writing all of the data should wake up the decoder to complete. + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + sourceBuffer->Complete(NS_OK); + + // It would have gotten posted to the main thread to avoid mutex contention. + SpinPendingEvents(); + + // Call the lambda to verify the expected results. + aResultChecker(decoder); +} + +static void CheckDecoderDelayedChunk(const ImageTestCase& aTestCase) { + WithDelayedChunkDecode(aTestCase, Nothing(), [&](image::Decoder* aDecoder) { + CheckDecoderResults(aTestCase, aDecoder); + }); +} + +static void CheckDecoderMultiChunk(const ImageTestCase& aTestCase, + uint64_t aChunkSize = 1) { + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Create a SourceBuffer and a decoder. + auto sourceBuffer = MakeNotNull>(); + sourceBuffer->ExpectLength(length); + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + DecoderFlags decoderFlags = + DecoderFactory::GetDefaultDecoderFlagsForType(decoderType) | + DecoderFlags::FIRST_FRAME_ONLY; + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, Nothing(), decoderFlags, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ true); + + // Run the full decoder synchronously. It should now be waiting on + // the iterator to yield some data since we haven't written anything yet. + task->Run(); + + while (length > 0) { + uint64_t read = length > aChunkSize ? aChunkSize : length; + length -= read; + + uint64_t available = 0; + rv = inputStream->Available(&available); + ASSERT_TRUE(available >= read); + ASSERT_NS_SUCCEEDED(rv); + + // Writing any data should wake up the decoder to complete. + rv = sourceBuffer->AppendFromInputStream(inputStream, read); + ASSERT_NS_SUCCEEDED(rv); + + // It would have gotten posted to the main thread to avoid mutex contention. + SpinPendingEvents(); + } + + sourceBuffer->Complete(NS_OK); + SpinPendingEvents(); + + CheckDecoderResults(aTestCase, decoder); +} + +static void CheckDownscaleDuringDecode(const ImageTestCase& aTestCase) { + // This function expects that |aTestCase| consists of 25 lines of green, + // followed by 25 lines of red, followed by 25 lines of green, followed by 25 + // more lines of red. We'll downscale it from 100x100 to 20x20. + IntSize outputSize(20, 20); + + WithSingleChunkDecode( + aTestCase, Some(outputSize), /* aUseDecodePool */ false, + [&](image::Decoder* aDecoder) { + RefPtr surface = CheckDecoderState(aTestCase, aDecoder); + + // There are no downscale-during-decode tests that have + // TEST_CASE_HAS_ERROR set, so we expect to always get a surface here. + EXPECT_TRUE(surface != nullptr); + + if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) { + return; + } + + // Check that the downscaled image is correct. Note that we skip rows + // near the transitions between colors, since the downscaler does not + // produce a sharp boundary at these points. Even some of the rows we + // test need a small amount of fuzz; this is just the nature of Lanczos + // downscaling. + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, + aTestCase.ChooseColor(BGRAColor::Green()), + /* aFuzz = */ 47)); + EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, + aTestCase.ChooseColor(BGRAColor::Red()), + /* aFuzz = */ 27)); + EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), + /* aFuzz = */ 47)); + EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, + aTestCase.ChooseColor(BGRAColor::Red()), + /* aFuzz = */ 27)); + }); +} + +static void CheckAnimationDecoderResults(const ImageTestCase& aTestCase, + AnimationSurfaceProvider* aProvider, + image::Decoder* aDecoder) { + EXPECT_TRUE(aDecoder->GetDecodeDone()); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), aDecoder->HasError()); + + if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) { + return; // That's all we can check for bad images. + } + + // The decoder should get the correct size. + OrientedIntSize size = aDecoder->Size(); + EXPECT_EQ(aTestCase.mSize.width, size.width); + EXPECT_EQ(aTestCase.mSize.height, size.height); + + if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) { + return; + } + + // Check the output. + AutoTArray framePixels; + framePixels.AppendElement(aTestCase.ChooseColor(BGRAColor::Green())); + framePixels.AppendElement( + aTestCase.ChooseColor(BGRAColor(0x7F, 0x7F, 0x7F, 0xFF))); + + DrawableSurface drawableSurface(WrapNotNull(aProvider)); + for (size_t i = 0; i < framePixels.Length(); ++i) { + nsresult rv = drawableSurface.Seek(i); + EXPECT_NS_SUCCEEDED(rv); + + // Check the first frame, all green. + RawAccessFrameRef rawFrame = drawableSurface->RawAccessRef(); + RefPtr surface = rawFrame->GetSourceSurface(); + + // Verify that the resulting surfaces matches our expectations. + EXPECT_TRUE(surface->IsDataSourceSurface()); + EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX || + surface->GetFormat() == SurfaceFormat::OS_RGBA); + EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize()); + EXPECT_TRUE(IsSolidColor(surface, framePixels[i], aTestCase.Fuzz())); + } + + // Should be no more frames. + nsresult rv = drawableSurface.Seek(framePixels.Length()); + EXPECT_NS_FAILED(rv); +} + +template +static void WithSingleChunkAnimationDecode(const ImageTestCase& aTestCase, + Func aResultChecker) { + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(aTestCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + NotNull> rasterImage = + WrapNotNull(static_cast(image.get())); + + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into a SourceBuffer. + NotNull> sourceBuffer = WrapNotNull(new SourceBuffer()); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + sourceBuffer->Complete(NS_OK); + + // Create a metadata decoder first, because otherwise RasterImage will get + // unhappy about finding out the image is animated during a full decode. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + DecoderFlags decoderFlags = + DecoderFactory::GetDefaultDecoderFlagsForType(decoderType); + RefPtr task = DecoderFactory::CreateMetadataDecoder( + decoderType, rasterImage, decoderFlags, sourceBuffer); + ASSERT_TRUE(task != nullptr); + + // Run the metadata decoder synchronously. + task->Run(); + + // Create a decoder. + SurfaceFlags surfaceFlags = aTestCase.mSurfaceFlags; + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, Nothing(), decoderFlags, surfaceFlags); + ASSERT_TRUE(decoder != nullptr); + + // Create an AnimationSurfaceProvider which will manage the decoding process + // and make this decoder's output available in the surface cache. + SurfaceKey surfaceKey = RasterSurfaceKey(aTestCase.mOutputSize, surfaceFlags, + PlaybackType::eAnimated); + RefPtr provider = new AnimationSurfaceProvider( + rasterImage, surfaceKey, WrapNotNull(decoder), + /* aCurrentFrame */ 0); + + // Run the full decoder synchronously. + provider->Run(); + + // Call the lambda to verify the expected results. + aResultChecker(provider, decoder); +} + +static void CheckAnimationDecoderSingleChunk(const ImageTestCase& aTestCase) { + WithSingleChunkAnimationDecode( + aTestCase, + [&](AnimationSurfaceProvider* aProvider, image::Decoder* aDecoder) { + CheckAnimationDecoderResults(aTestCase, aProvider, aDecoder); + }); +} + +static void CheckDecoderFrameFirst(const ImageTestCase& aTestCase) { + // Verify that we can decode this test case and retrieve the first frame using + // imgIContainer::FRAME_FIRST. This ensures that we correctly trigger a + // single-frame decode rather than an animated decode when + // imgIContainer::FRAME_FIRST is requested. + + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(aTestCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + ASSERT_NS_SUCCEEDED(rv); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + // Lock the image so its surfaces don't disappear during the test. + image->LockImage(); + + auto unlock = mozilla::MakeScopeExit([&] { image->UnlockImage(); }); + + // Use GetFrame() to force a sync decode of the image, specifying FRAME_FIRST + // to ensure that we don't get an animated decode. + RefPtr surface = image->GetFrame( + imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that the image's metadata meets our expectations. + IntSize imageSize(0, 0); + rv = image->GetWidth(&imageSize.width); + EXPECT_NS_SUCCEEDED(rv); + rv = image->GetHeight(&imageSize.height); + EXPECT_NS_SUCCEEDED(rv); + + EXPECT_EQ(aTestCase.mSize.width, imageSize.width); + EXPECT_EQ(aTestCase.mSize.height, imageSize.height); + + Progress imageProgress = tracker->GetProgress(); + + EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false); + EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true); + + // Ensure that we decoded the static version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eStatic), + /* aMarkUsed = */ false); + ASSERT_EQ(MatchType::EXACT, result.Type()); + EXPECT_TRUE(bool(result.Surface())); + } + + // Ensure that we didn't decode the animated version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ false); + ASSERT_EQ(MatchType::NOT_FOUND, result.Type()); + } + + // Use GetFrame() to force a sync decode of the image, this time specifying + // FRAME_CURRENT to ensure that we get an animated decode. + RefPtr animatedSurface = image->GetFrame( + imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that we decoded both frames of the animated version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + + EXPECT_NS_SUCCEEDED(result.Surface().Seek(0)); + EXPECT_TRUE(bool(result.Surface())); + + RefPtr partialFrame = result.Surface().GetFrame(1); + EXPECT_TRUE(bool(partialFrame)); + } + + // Ensure that the static version is still around. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eStatic), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + EXPECT_TRUE(bool(result.Surface())); + } +} + +static void CheckDecoderFrameCurrent(const ImageTestCase& aTestCase) { + // Verify that we can decode this test case and retrieve the entire sequence + // of frames using imgIContainer::FRAME_CURRENT. This ensures that we + // correctly trigger an animated decode rather than a single-frame decode when + // imgIContainer::FRAME_CURRENT is requested. + + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(aTestCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + ASSERT_NS_SUCCEEDED(rv); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + // Lock the image so its surfaces don't disappear during the test. + image->LockImage(); + + // Use GetFrame() to force a sync decode of the image, specifying + // FRAME_CURRENT to ensure we get an animated decode. + RefPtr surface = image->GetFrame( + imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that the image's metadata meets our expectations. + IntSize imageSize(0, 0); + rv = image->GetWidth(&imageSize.width); + EXPECT_NS_SUCCEEDED(rv); + rv = image->GetHeight(&imageSize.height); + EXPECT_NS_SUCCEEDED(rv); + + EXPECT_EQ(aTestCase.mSize.width, imageSize.width); + EXPECT_EQ(aTestCase.mSize.height, imageSize.height); + + Progress imageProgress = tracker->GetProgress(); + + EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false); + EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true); + + // Ensure that we decoded both frames of the animated version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + + EXPECT_NS_SUCCEEDED(result.Surface().Seek(0)); + EXPECT_TRUE(bool(result.Surface())); + + RefPtr partialFrame = result.Surface().GetFrame(1); + EXPECT_TRUE(bool(partialFrame)); + } + + // Ensure that we didn't decode the static version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eStatic), + /* aMarkUsed = */ false); + ASSERT_EQ(MatchType::NOT_FOUND, result.Type()); + } + + // Use GetFrame() to force a sync decode of the image, this time specifying + // FRAME_FIRST to ensure that we get a single-frame decode. + RefPtr animatedSurface = image->GetFrame( + imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that we decoded the static version of the image. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eStatic), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + EXPECT_TRUE(bool(result.Surface())); + } + + // Ensure that both frames of the animated version are still around. + { + LookupResult result = SurfaceCache::Lookup( + ImageKey(image.get()), + RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + + EXPECT_NS_SUCCEEDED(result.Surface().Seek(0)); + EXPECT_TRUE(bool(result.Surface())); + + RefPtr partialFrame = result.Surface().GetFrame(1); + EXPECT_TRUE(bool(partialFrame)); + } +} + +class ImageDecoders : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +#define IMAGE_GTEST_DECODER_BASE_F(test_prefix) \ + TEST_F(ImageDecoders, test_prefix##SingleChunk) { \ + CheckDecoderSingleChunk(Green##test_prefix##TestCase()); \ + } \ + \ + TEST_F(ImageDecoders, test_prefix##DelayedChunk) { \ + CheckDecoderDelayedChunk(Green##test_prefix##TestCase()); \ + } \ + \ + TEST_F(ImageDecoders, test_prefix##MultiChunk) { \ + CheckDecoderMultiChunk(Green##test_prefix##TestCase()); \ + } \ + \ + TEST_F(ImageDecoders, test_prefix##DownscaleDuringDecode) { \ + CheckDownscaleDuringDecode(Downscaled##test_prefix##TestCase()); \ + } \ + \ + TEST_F(ImageDecoders, test_prefix##ForceSRGB) { \ + CheckDecoderSingleChunk(Green##test_prefix##TestCase().WithSurfaceFlags( \ + SurfaceFlags::TO_SRGB_COLORSPACE)); \ + } \ + \ + TEST_F(ImageDecoders, test_prefix##BadBuffer) { \ + CheckDecoderBadBuffer(Green##test_prefix##TestCase().WithFlags( \ + TEST_CASE_HAS_ERROR | TEST_CASE_IGNORE_OUTPUT)); \ + } + +IMAGE_GTEST_DECODER_BASE_F(PNG) +IMAGE_GTEST_DECODER_BASE_F(GIF) +IMAGE_GTEST_DECODER_BASE_F(JPG) +IMAGE_GTEST_DECODER_BASE_F(BMP) +IMAGE_GTEST_DECODER_BASE_F(ICO) +IMAGE_GTEST_DECODER_BASE_F(Icon) +IMAGE_GTEST_DECODER_BASE_F(WebP) +#ifdef MOZ_JXL +IMAGE_GTEST_DECODER_BASE_F(JXL) +#endif + +TEST_F(ImageDecoders, ICOWithANDMaskDownscaleDuringDecode) { + CheckDownscaleDuringDecode(DownscaledTransparentICOWithANDMaskTestCase()); +} + +TEST_F(ImageDecoders, WebPLargeMultiChunk) { + CheckDecoderMultiChunk(LargeWebPTestCase(), /* aChunkSize */ 64); +} + +TEST_F(ImageDecoders, WebPIccSrgbMultiChunk) { + CheckDecoderMultiChunk(GreenWebPIccSrgbTestCase()); +} + +TEST_F(ImageDecoders, WebPTransparentSingleChunk) { + CheckDecoderSingleChunk(TransparentWebPTestCase()); +} + +TEST_F(ImageDecoders, WebPTransparentNoAlphaHeaderSingleChunk) { + CheckDecoderSingleChunk(TransparentNoAlphaHeaderWebPTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunk) { + CheckDecoderSingleChunk(GreenAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkNonzeroReserved) { + CheckDecoderSingleChunk(NonzeroReservedAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkMultipleColr) { + CheckDecoderSingleChunk(MultipleColrAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent10bit420) { + CheckDecoderSingleChunk(Transparent10bit420AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent10bit422) { + CheckDecoderSingleChunk(Transparent10bit422AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent10bit444) { + CheckDecoderSingleChunk(Transparent10bit444AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent12bit420) { + CheckDecoderSingleChunk(Transparent12bit420AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent12bit422) { + CheckDecoderSingleChunk(Transparent12bit422AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent12bit444) { + CheckDecoderSingleChunk(Transparent12bit444AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent8bit420) { + CheckDecoderSingleChunk(Transparent8bit420AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent8bit422) { + CheckDecoderSingleChunk(Transparent8bit422AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkTransparent8bit444) { + CheckDecoderSingleChunk(Transparent8bit444AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitLimitedRangeBT601) { + CheckDecoderSingleChunk(Gray8bitLimitedRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitLimitedRangeBT709) { + CheckDecoderSingleChunk(Gray8bitLimitedRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitLimitedRangeBT2020) { + CheckDecoderSingleChunk(Gray8bitLimitedRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitFullRangeBT601) { + CheckDecoderSingleChunk(Gray8bitFullRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitFullRangeBT709) { + CheckDecoderSingleChunk(Gray8bitFullRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitFullRangeBT2020) { + CheckDecoderSingleChunk(Gray8bitFullRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitLimitedRangeBT601) { + CheckDecoderSingleChunk(Gray10bitLimitedRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitLimitedRangeBT709) { + CheckDecoderSingleChunk(Gray10bitLimitedRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitLimitedRangeBT2020) { + CheckDecoderSingleChunk(Gray10bitLimitedRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitFullRangeBT601) { + CheckDecoderSingleChunk(Gray10bitFullRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitFullRangeBT709) { + CheckDecoderSingleChunk(Gray10bitFullRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitFullRangeBT2020) { + CheckDecoderSingleChunk(Gray10bitFullRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitLimitedRangeBT601) { + CheckDecoderSingleChunk(Gray12bitLimitedRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitLimitedRangeBT709) { + CheckDecoderSingleChunk(Gray12bitLimitedRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitLimitedRangeBT2020) { + CheckDecoderSingleChunk(Gray12bitLimitedRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitFullRangeBT601) { + CheckDecoderSingleChunk(Gray12bitFullRangeBT601AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitFullRangeBT709) { + CheckDecoderSingleChunk(Gray12bitFullRangeBT709AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitFullRangeBT2020) { + CheckDecoderSingleChunk(Gray12bitFullRangeBT2020AVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitLimitedRangeGrayscale) { + CheckDecoderSingleChunk(Gray8bitLimitedRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray8bitFullRangeGrayscale) { + CheckDecoderSingleChunk(Gray8bitFullRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitLimitedRangeGrayscale) { + CheckDecoderSingleChunk(Gray10bitLimitedRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray10bitFullRangeGrayscale) { + CheckDecoderSingleChunk(Gray10bitFullRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitLimitedRangeGrayscale) { + CheckDecoderSingleChunk(Gray12bitLimitedRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFSingleChunkGray12bitFullRangeGrayscale) { + CheckDecoderSingleChunk(Gray12bitFullRangeGrayscaleAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFMultiLayerSingleChunk) { + CheckDecoderSingleChunk(MultiLayerAVIFTestCase()); +} + +// This test must use the decode pool in order to check for regressions +// of crashing the dav1d decoder when the ImgDecoder threads have a standard- +// sized stack. +TEST_F(ImageDecoders, AVIFStackCheck) { + CheckDecoderSingleChunk(StackCheckAVIFTestCase(), /* aUseDecodePool */ true); +} + +TEST_F(ImageDecoders, AVIFDelayedChunk) { + CheckDecoderDelayedChunk(GreenAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFMultiChunk) { + CheckDecoderMultiChunk(GreenAVIFTestCase()); +} + +TEST_F(ImageDecoders, AVIFLargeMultiChunk) { + CheckDecoderMultiChunk(LargeAVIFTestCase(), /* aChunkSize */ 64); +} + +TEST_F(ImageDecoders, AVIFDownscaleDuringDecode) { + CheckDownscaleDuringDecode(DownscaledAVIFTestCase()); +} + +#ifdef MOZ_JXL +TEST_F(ImageDecoders, JXLLargeMultiChunk) { + CheckDecoderMultiChunk(LargeJXLTestCase(), /* aChunkSize */ 64); +} +#endif + +TEST_F(ImageDecoders, AnimatedGIFSingleChunk) { + CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedGIFMultiChunk) { + CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedGIFWithBlendedFrames) { + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedPNGSingleChunk) { + CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST_F(ImageDecoders, AnimatedPNGMultiChunk) { + CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST_F(ImageDecoders, AnimatedPNGWithBlendedFrames) { + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST_F(ImageDecoders, AnimatedWebPSingleChunk) { + CheckDecoderSingleChunk(GreenFirstFrameAnimatedWebPTestCase()); +} + +TEST_F(ImageDecoders, AnimatedWebPMultiChunk) { + CheckDecoderMultiChunk(GreenFirstFrameAnimatedWebPTestCase()); +} + +TEST_F(ImageDecoders, AnimatedWebPWithBlendedFrames) { + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedWebPTestCase()); +} + +TEST_F(ImageDecoders, AnimatedAVIFSingleChunk) { + CheckDecoderSingleChunk(GreenFirstFrameAnimatedAVIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedAVIFMultiChunk) { + CheckDecoderMultiChunk(GreenFirstFrameAnimatedAVIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedAVIFWithBlendedFrames) { + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedAVIFTestCase()); +} + +TEST_F(ImageDecoders, CorruptSingleChunk) { + CheckDecoderSingleChunk(CorruptTestCase()); +} + +TEST_F(ImageDecoders, CorruptMultiChunk) { + CheckDecoderMultiChunk(CorruptTestCase()); +} + +TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderSingleChunk) { + CheckDecoderSingleChunk(CorruptBMPWithTruncatedHeader()); +} + +TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderMultiChunk) { + CheckDecoderMultiChunk(CorruptBMPWithTruncatedHeader()); +} + +TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthSingleChunk) { + CheckDecoderSingleChunk(CorruptICOWithBadBMPWidthTestCase()); +} + +TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthMultiChunk) { + CheckDecoderMultiChunk(CorruptICOWithBadBMPWidthTestCase()); +} + +TEST_F(ImageDecoders, CorruptICOWithBadBMPHeightSingleChunk) { + CheckDecoderSingleChunk(CorruptICOWithBadBMPHeightTestCase()); +} + +TEST_F(ImageDecoders, CorruptICOWithBadBMPHeightMultiChunk) { + CheckDecoderMultiChunk(CorruptICOWithBadBMPHeightTestCase()); +} + +TEST_F(ImageDecoders, CorruptICOWithBadBppSingleChunk) { + CheckDecoderSingleChunk(CorruptICOWithBadBppTestCase()); +} + +// Running this test under emulation for Android 7 on x86_64 seems to result +// in the large allocation succeeding, but leaving so little memory left the +// system falls over and it kills the test run, so we skip it instead. +// See bug 1655846 for more details. +#ifndef ANDROID +TEST_F(ImageDecoders, CorruptAVIFSingleChunk) { + CheckDecoderSingleChunk(CorruptAVIFTestCase()); +} +#endif + +TEST_F(ImageDecoders, AnimatedGIFWithFRAME_FIRST) { + CheckDecoderFrameFirst(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedGIFWithFRAME_CURRENT) { + CheckDecoderFrameCurrent(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoders, AnimatedGIFWithExtraImageSubBlocks) { + ImageTestCase testCase = ExtraImageSubBlocksAnimatedGIFTestCase(); + + // Verify that we can decode this test case and get two frames, even though + // there are extra image sub blocks between the first and second frame. The + // extra data shouldn't confuse the decoder or cause the decode to fail. + + // Create an image. + RefPtr image = TestCaseToDecodedImage(testCase); + + // Ensure that the image's metadata meets our expectations. + IntSize imageSize(0, 0); + nsresult rv = image->GetWidth(&imageSize.width); + EXPECT_NS_SUCCEEDED(rv); + rv = image->GetHeight(&imageSize.height); + EXPECT_NS_SUCCEEDED(rv); + + EXPECT_EQ(testCase.mSize.width, imageSize.width); + EXPECT_EQ(testCase.mSize.height, imageSize.height); + + RefPtr tracker = image->GetProgressTracker(); + Progress imageProgress = tracker->GetProgress(); + + EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false); + EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true); + + // Ensure that we decoded both frames of the image. + LookupResult result = + SurfaceCache::Lookup(ImageKey(image.get()), + RasterSurfaceKey(imageSize, testCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + + EXPECT_NS_SUCCEEDED(result.Surface().Seek(0)); + EXPECT_TRUE(bool(result.Surface())); + + RefPtr partialFrame = result.Surface().GetFrame(1); + EXPECT_TRUE(bool(partialFrame)); +} + +TEST_F(ImageDecoders, AnimatedWebPWithFRAME_FIRST) { + CheckDecoderFrameFirst(GreenFirstFrameAnimatedWebPTestCase()); +} + +TEST_F(ImageDecoders, AnimatedWebPWithFRAME_CURRENT) { + CheckDecoderFrameCurrent(GreenFirstFrameAnimatedWebPTestCase()); +} + +TEST_F(ImageDecoders, TruncatedSmallGIFSingleChunk) { + CheckDecoderSingleChunk(TruncatedSmallGIFTestCase()); +} + +TEST_F(ImageDecoders, LargeICOWithBMPSingleChunk) { + CheckDecoderSingleChunk(LargeICOWithBMPTestCase()); +} + +TEST_F(ImageDecoders, LargeICOWithBMPMultiChunk) { + CheckDecoderMultiChunk(LargeICOWithBMPTestCase(), /* aChunkSize */ 64); +} + +TEST_F(ImageDecoders, LargeICOWithPNGSingleChunk) { + CheckDecoderSingleChunk(LargeICOWithPNGTestCase()); +} + +TEST_F(ImageDecoders, LargeICOWithPNGMultiChunk) { + CheckDecoderMultiChunk(LargeICOWithPNGTestCase()); +} + +TEST_F(ImageDecoders, MultipleSizesICOSingleChunk) { + ImageTestCase testCase = GreenMultipleSizesICOTestCase(); + + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(testCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(testCase.mPath); + ASSERT_TRUE(inputStream); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + ASSERT_NS_SUCCEEDED(rv); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + // Use GetFrame() to force a sync decode of the image. + RefPtr surface = image->GetFrame( + imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that the image's metadata meets our expectations. + IntSize imageSize(0, 0); + rv = image->GetWidth(&imageSize.width); + EXPECT_NS_SUCCEEDED(rv); + rv = image->GetHeight(&imageSize.height); + EXPECT_NS_SUCCEEDED(rv); + + EXPECT_EQ(testCase.mSize.width, imageSize.width); + EXPECT_EQ(testCase.mSize.height, imageSize.height); + + nsTArray nativeSizes; + rv = image->GetNativeSizes(nativeSizes); + EXPECT_NS_SUCCEEDED(rv); + ASSERT_EQ(6u, nativeSizes.Length()); + + IntSize expectedSizes[] = {IntSize(16, 16), IntSize(32, 32), + IntSize(64, 64), IntSize(128, 128), + IntSize(256, 256), IntSize(256, 128)}; + + for (int i = 0; i < 6; ++i) { + EXPECT_EQ(expectedSizes[i], nativeSizes[i]); + } + + RefPtr image90 = + ImageOps::Orient(image, Orientation(Angle::D90, Flip::Unflipped)); + rv = image90->GetNativeSizes(nativeSizes); + EXPECT_NS_SUCCEEDED(rv); + ASSERT_EQ(6u, nativeSizes.Length()); + + for (int i = 0; i < 5; ++i) { + EXPECT_EQ(expectedSizes[i], nativeSizes[i]); + } + EXPECT_EQ(IntSize(128, 256), nativeSizes[5]); + + RefPtr image180 = + ImageOps::Orient(image, Orientation(Angle::D180, Flip::Unflipped)); + rv = image180->GetNativeSizes(nativeSizes); + EXPECT_NS_SUCCEEDED(rv); + ASSERT_EQ(6u, nativeSizes.Length()); + + for (int i = 0; i < 6; ++i) { + EXPECT_EQ(expectedSizes[i], nativeSizes[i]); + } +} + +TEST_F(ImageDecoders, ExifResolutionEven) { + RefPtr image = TestCaseToDecodedImage(ExifResolutionTestCase()); + EXPECT_EQ(image->GetResolution(), Resolution(2.0, 2.0)); +} diff --git a/image/test/gtest/TestDecodersPerf.cpp b/image/test/gtest/TestDecodersPerf.cpp new file mode 100644 index 0000000000..e1d6bfbded --- /dev/null +++ b/image/test/gtest/TestDecodersPerf.cpp @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "gtest/MozGTestBench.h" + +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "IDecodingTask.h" +#include "mozilla/RefPtr.h" +#include "ProgressTracker.h" +#include "SourceBuffer.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace { + +static void CheckDecoderState(const ImageTestCase& aTestCase, + image::Decoder* aDecoder, + const IntSize& aOutputSize) { + // image::Decoder should match what we asked for in the MIME type. + EXPECT_NE(aDecoder->GetType(), DecoderType::UNKNOWN); + EXPECT_EQ(aDecoder->GetType(), + DecoderFactory::GetDecoderType(aTestCase.mMimeType)); + + EXPECT_TRUE(aDecoder->GetDecodeDone()); + EXPECT_FALSE(aDecoder->HasError()); + + // Verify that the decoder made the expected progress. + Progress progress = aDecoder->TakeProgress(); + EXPECT_FALSE(bool(progress & FLAG_HAS_ERROR)); + EXPECT_FALSE(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR)); + + EXPECT_TRUE(bool(progress & FLAG_SIZE_AVAILABLE)); + EXPECT_TRUE(bool(progress & FLAG_DECODE_COMPLETE)); + EXPECT_TRUE(bool(progress & FLAG_FRAME_COMPLETE)); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_TRANSPARENT), + bool(progress & FLAG_HAS_TRANSPARENCY)); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED), + bool(progress & FLAG_IS_ANIMATED)); + + // The decoder should get the correct size. + OrientedIntSize size = aDecoder->Size(); + EXPECT_EQ(aTestCase.mSize.width, size.width); + EXPECT_EQ(aTestCase.mSize.height, size.height); + + // Get the current frame, which is always the first frame of the image + // because CreateAnonymousDecoder() forces a first-frame-only decode. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + // Verify that the resulting surfaces matches our expectations. + EXPECT_TRUE(surface->IsDataSourceSurface()); + EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX || + surface->GetFormat() == SurfaceFormat::OS_RGBA); + EXPECT_EQ(aOutputSize, surface->GetSize()); +} + +template +static void WithSingleChunkDecode(const ImageTestCase& aTestCase, + SourceBuffer* aSourceBuffer, + const Maybe& aOutputSize, + Func aResultChecker) { + auto sourceBuffer = WrapNotNull(RefPtr(aSourceBuffer)); + + // Create a decoder. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + RefPtr decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + + // Run the full decoder synchronously. + task->Run(); + + // Call the lambda to verify the expected results. + aResultChecker(decoder); +} + +static void CheckDecode(const ImageTestCase& aTestCase, + SourceBuffer* aSourceBuffer) { + WithSingleChunkDecode( + aTestCase, aSourceBuffer, Nothing(), [&](image::Decoder* aDecoder) { + CheckDecoderState(aTestCase, aDecoder, aTestCase.mSize); + }); +} + +static void CheckDownscaleDuringDecode(const ImageTestCase& aTestCase, + SourceBuffer* aSourceBuffer) { + IntSize outputSize(20, 20); + WithSingleChunkDecode(aTestCase, aSourceBuffer, Some(outputSize), + [&](image::Decoder* aDecoder) { + CheckDecoderState(aTestCase, aDecoder, outputSize); + }); +} + +#define IMAGE_GTEST_BENCH_FIXTURE(test_fixture, test_case) \ + class test_fixture : public ImageBenchmarkBase { \ + protected: \ + test_fixture() : ImageBenchmarkBase(test_case()) {} \ + }; + +#define IMAGE_GTEST_NATIVE_BENCH_F(test_fixture) \ + MOZ_GTEST_BENCH_F(test_fixture, Native, \ + [this] { CheckDecode(mTestCase, mSourceBuffer); }); + +#define IMAGE_GTEST_DOWNSCALE_BENCH_F(test_fixture) \ + MOZ_GTEST_BENCH_F(test_fixture, Downscale, [this] { \ + CheckDownscaleDuringDecode(mTestCase, mSourceBuffer); \ + }); + +#define IMAGE_GTEST_NO_COLOR_MANAGEMENT_BENCH_F(test_fixture) \ + MOZ_GTEST_BENCH_F(test_fixture, NoColorManagement, [this] { \ + ImageTestCase testCase = mTestCase; \ + testCase.mSurfaceFlags |= SurfaceFlags::NO_COLORSPACE_CONVERSION; \ + CheckDecode(testCase, mSourceBuffer); \ + }); + +#define IMAGE_GTEST_NO_PREMULTIPLY_BENCH_F(test_fixture) \ + MOZ_GTEST_BENCH_F(test_fixture, NoPremultiplyAlpha, [this] { \ + ImageTestCase testCase = mTestCase; \ + testCase.mSurfaceFlags |= SurfaceFlags::NO_PREMULTIPLY_ALPHA; \ + CheckDecode(testCase, mSourceBuffer); \ + }); + +#define IMAGE_GTEST_BENCH_F(type, test) \ + IMAGE_GTEST_BENCH_FIXTURE(ImageDecodersPerf_##type##_##test, \ + Perf##test##type##TestCase) \ + IMAGE_GTEST_NATIVE_BENCH_F(ImageDecodersPerf_##type##_##test) \ + IMAGE_GTEST_DOWNSCALE_BENCH_F(ImageDecodersPerf_##type##_##test) \ + IMAGE_GTEST_NO_COLOR_MANAGEMENT_BENCH_F(ImageDecodersPerf_##type##_##test) + +#define IMAGE_GTEST_BENCH_ALPHA_F(type, test) \ + IMAGE_GTEST_BENCH_F(type, test) \ + IMAGE_GTEST_NO_PREMULTIPLY_BENCH_F(ImageDecodersPerf_##type##_##test) + +IMAGE_GTEST_BENCH_F(JPG, YCbCr) +IMAGE_GTEST_BENCH_F(JPG, Cmyk) +IMAGE_GTEST_BENCH_F(JPG, Gray) + +IMAGE_GTEST_BENCH_F(PNG, Rgb) +IMAGE_GTEST_BENCH_F(PNG, Gray) +IMAGE_GTEST_BENCH_ALPHA_F(PNG, RgbAlpha) +IMAGE_GTEST_BENCH_ALPHA_F(PNG, GrayAlpha) + +IMAGE_GTEST_BENCH_F(WebP, RgbLossless) +IMAGE_GTEST_BENCH_F(WebP, RgbLossy) +IMAGE_GTEST_BENCH_ALPHA_F(WebP, RgbAlphaLossless) +IMAGE_GTEST_BENCH_ALPHA_F(WebP, RgbAlphaLossy) + +IMAGE_GTEST_BENCH_F(GIF, Rgb) + +} // namespace diff --git a/image/test/gtest/TestDeinterlacingFilter.cpp b/image/test/gtest/TestDeinterlacingFilter.cpp new file mode 100644 index 0000000000..fc3e6f65bd --- /dev/null +++ b/image/test/gtest/TestDeinterlacingFilter.cpp @@ -0,0 +1,636 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +template +void WithDeinterlacingFilter(const IntSize& aSize, bool aProgressiveDisplay, + Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(bool(decoder)); + + WithFilterPipeline( + decoder, std::forward(aFunc), + DeinterlacingConfig{aProgressiveDisplay}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +void AssertConfiguringDeinterlacingFilterFails(const IntSize& aSize) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AssertConfiguringPipelineFails( + decoder, DeinterlacingConfig{/* mProgressiveDisplay = */ true}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +class ImageDeinterlacingFilter : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageDeinterlacingFilter, WritePixels100_100) { + WithDeinterlacingFilter( + IntSize(100, 100), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixels99_99) { + WithDeinterlacingFilter(IntSize(99, 99), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 99, 99)), + /* aInputRect = */ Some(IntRect(0, 0, 99, 99))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixels8_8) { + WithDeinterlacingFilter(IntSize(8, 8), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 8, 8)), + /* aInputRect = */ Some(IntRect(0, 0, 8, 8))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixels7_7) { + WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 7, 7)), + /* aInputRect = */ Some(IntRect(0, 0, 7, 7))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixels3_3) { + WithDeinterlacingFilter(IntSize(3, 3), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 3, 3)), + /* aInputRect = */ Some(IntRect(0, 0, 3, 3))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixels1_1) { + WithDeinterlacingFilter(IntSize(1, 1), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)), + /* aInputRect = */ Some(IntRect(0, 0, 1, 1))); + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsNonProgressiveOutput51_52) { + WithDeinterlacingFilter( + IntSize(51, 52), /* aProgressiveDisplay = */ false, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be green for even rows and red for + // odd rows but we need to write the rows in the order that the + // deinterlacer expects them. + uint32_t count = 0; + auto result = aFilter->WritePixels([&]() { + uint32_t row = count / 51; // Integer division. + ++count; + + // Note that we use a switch statement here, even though it's quite + // verbose, because it's useful to have the mappings between input and + // output rows available when debugging these tests. + + switch (row) { + // First pass. Output rows are positioned at 8n + 0. + case 0: // Output row 0. + case 1: // Output row 8. + case 2: // Output row 16. + case 3: // Output row 24. + case 4: // Output row 32. + case 5: // Output row 40. + case 6: // Output row 48. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Second pass. Rows are positioned at 8n + 4. + case 7: // Output row 4. + case 8: // Output row 12. + case 9: // Output row 20. + case 10: // Output row 28. + case 11: // Output row 36. + case 12: // Output row 44. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Third pass. Rows are positioned at 4n + 2. + case 13: // Output row 2. + case 14: // Output row 6. + case 15: // Output row 10. + case 16: // Output row 14. + case 17: // Output row 18. + case 18: // Output row 22. + case 19: // Output row 26. + case 20: // Output row 30. + case 21: // Output row 34. + case 22: // Output row 38. + case 23: // Output row 42. + case 24: // Output row 46. + case 25: // Output row 50. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Fourth pass. Rows are positioned at 2n + 1. + case 26: // Output row 1. + case 27: // Output row 3. + case 28: // Output row 5. + case 29: // Output row 7. + case 30: // Output row 9. + case 31: // Output row 11. + case 32: // Output row 13. + case 33: // Output row 15. + case 34: // Output row 17. + case 35: // Output row 19. + case 36: // Output row 21. + case 37: // Output row 23. + case 38: // Output row 25. + case 39: // Output row 27. + case 40: // Output row 29. + case 41: // Output row 31. + case 42: // Output row 33. + case 43: // Output row 35. + case 44: // Output row 37. + case 45: // Output row 39. + case 46: // Output row 41. + case 47: // Output row 43. + case 48: // Output row 45. + case 49: // Output row 47. + case 50: // Output row 49. + case 51: // Output row 51. + return AsVariant(BGRAColor::Red().AsPixel()); + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected row"); + return AsVariant(BGRAColor::Transparent().AsPixel()); + } + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(51u * 52u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 51, 52), + IntRect(0, 0, 51, 52)); + + // Check that the generated image is correct. As mentioned above, we + // expect even rows to be green and odd rows to be red. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 52; ++row) { + EXPECT_TRUE(RowsAreSolidColor( + surface, row, 1, + row % 2 == 0 ? BGRAColor::Green() : BGRAColor::Red())); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsOutput20_20) { + WithDeinterlacingFilter( + IntSize(20, 20), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be green for even rows and red for + // odd rows but we need to write the rows in the order that the + // deinterlacer expects them. + uint32_t count = 0; + auto result = aFilter->WritePixels([&]() { + uint32_t row = count / 20; // Integer division. + ++count; + + // Note that we use a switch statement here, even though it's quite + // verbose, because it's useful to have the mappings between input and + // output rows available when debugging these tests. + + switch (row) { + // First pass. Output rows are positioned at 8n + 0. + case 0: // Output row 0. + case 1: // Output row 8. + case 2: // Output row 16. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Second pass. Rows are positioned at 8n + 4. + case 3: // Output row 4. + case 4: // Output row 12. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Third pass. Rows are positioned at 4n + 2. + case 5: // Output row 2. + case 6: // Output row 6. + case 7: // Output row 10. + case 8: // Output row 14. + case 9: // Output row 18. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Fourth pass. Rows are positioned at 2n + 1. + case 10: // Output row 1. + case 11: // Output row 3. + case 12: // Output row 5. + case 13: // Output row 7. + case 14: // Output row 9. + case 15: // Output row 11. + case 16: // Output row 13. + case 17: // Output row 15. + case 18: // Output row 17. + case 19: // Output row 19. + return AsVariant(BGRAColor::Red().AsPixel()); + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected row"); + return AsVariant(BGRAColor::Transparent().AsPixel()); + } + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(20u * 20u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 20, 20), + IntRect(0, 0, 20, 20)); + + // Check that the generated image is correct. As mentioned above, we + // expect even rows to be green and odd rows to be red. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 20; ++row) { + EXPECT_TRUE(RowsAreSolidColor( + surface, row, 1, + row % 2 == 0 ? BGRAColor::Green() : BGRAColor::Red())); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsOutput7_7) { + WithDeinterlacingFilter( + IntSize(7, 7), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be a repeating pattern of two green + // rows followed by two red rows but we need to write the rows in the + // order that the deinterlacer expects them. + uint32_t count = 0; + auto result = aFilter->WritePixels([&]() { + uint32_t row = count / 7; // Integer division. + ++count; + + switch (row) { + // First pass. Output rows are positioned at 8n + 0. + case 0: // Output row 0. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Second pass. Rows are positioned at 8n + 4. + case 1: // Output row 4. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Third pass. Rows are positioned at 4n + 2. + case 2: // Output row 2. + case 3: // Output row 6. + return AsVariant(BGRAColor::Red().AsPixel()); + + // Fourth pass. Rows are positioned at 2n + 1. + case 4: // Output row 1. + return AsVariant(BGRAColor::Green().AsPixel()); + + case 5: // Output row 3. + return AsVariant(BGRAColor::Red().AsPixel()); + + case 6: // Output row 5. + return AsVariant(BGRAColor::Green().AsPixel()); + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected row"); + return AsVariant(BGRAColor::Transparent().AsPixel()); + } + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(7u * 7u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 7, 7), + IntRect(0, 0, 7, 7)); + + // Check that the generated image is correct. As mentioned above, we + // expect two green rows, followed by two red rows, then two green rows, + // etc. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 7; ++row) { + BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5 + ? BGRAColor::Green() + : BGRAColor::Red(); + EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color)); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsOutput3_3) { + WithDeinterlacingFilter( + IntSize(3, 3), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be green, red, green in that order, + // but we need to write the rows in the order that the deinterlacer + // expects them. + uint32_t count = 0; + auto result = aFilter->WritePixels([&]() { + uint32_t row = count / 3; // Integer division. + ++count; + + switch (row) { + // First pass. Output rows are positioned at 8n + 0. + case 0: // Output row 0. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Second pass. Rows are positioned at 8n + 4. + // No rows for this pass. + + // Third pass. Rows are positioned at 4n + 2. + case 1: // Output row 2. + return AsVariant(BGRAColor::Green().AsPixel()); + + // Fourth pass. Rows are positioned at 2n + 1. + case 2: // Output row 1. + return AsVariant(BGRAColor::Red().AsPixel()); + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected row"); + return AsVariant(BGRAColor::Transparent().AsPixel()); + } + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(3u * 3u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 3, 3), + IntRect(0, 0, 3, 3)); + + // Check that the generated image is correct. As mentioned above, we + // expect green, red, green in that order. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 3; ++row) { + EXPECT_TRUE(RowsAreSolidColor( + surface, row, 1, + row == 0 || row == 2 ? BGRAColor::Green() : BGRAColor::Red())); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsOutput1_1) { + WithDeinterlacingFilter( + IntSize(1, 1), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be a single red row. + uint32_t count = 0; + auto result = aFilter->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(1u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 1, 1), + IntRect(0, 0, 1, 1)); + + // Check that the generated image is correct. As mentioned above, we + // expect a single red row. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 1, BGRAColor::Red())); + }); +} + +void WriteRowAndCheckInterlacerOutput(image::Decoder* aDecoder, + SurfaceFilter* aFilter, BGRAColor aColor, + WriteState aNextState, + OrientedIntRect aInvalidRect, + uint32_t aFirstHaeberliRow, + uint32_t aLastHaeberliRow) { + uint32_t count = 0; + + auto result = aFilter->WritePixels([&]() -> NextPixel { + if (count < 7) { + ++count; + return AsVariant(aColor.AsPixel()); + } + return AsVariant(WriteState::NEED_MORE_DATA); + }); + + EXPECT_EQ(aNextState, result); + EXPECT_EQ(7u, count); + + // Assert that we got the expected invalidation region. + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(aInvalidRect, invalidRect->mInputSpaceRect); + EXPECT_EQ(aInvalidRect, invalidRect->mOutputSpaceRect); + + // Check that the portion of the image generated so far is correct. The rows + // from aFirstHaeberliRow to aLastHaeberliRow should be filled with aColor. + // Note that this is not the same as the set of rows in aInvalidRect, because + // after writing a row the deinterlacer seeks to the next row to write, which + // may involve copying previously-written rows in the buffer to the output + // even though they don't change in this pass. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = aFirstHaeberliRow; row <= aLastHaeberliRow; ++row) { + EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, aColor)); + } +} + +TEST_F(ImageDeinterlacingFilter, WritePixelsIntermediateOutput7_7) { + WithDeinterlacingFilter( + IntSize(7, 7), /* aProgressiveDisplay = */ true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be a repeating pattern of two green + // rows followed by two red rows but we need to write the rows in the + // order that the deinterlacer expects them. + + // First pass. Output rows are positioned at 8n + 0. + + // Output row 0. The invalid rect is the entire image because this is + // the end of the first pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 0, 4); + + // Second pass. Rows are positioned at 8n + 4. + + // Output row 4. The invalid rect is the entire image because this is + // the end of the second pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 1, 4); + + // Third pass. Rows are positioned at 4n + 2. + + // Output row 2. The invalid rect contains the Haeberli rows for this + // output row (rows 2 and 3) as well as the rows that we copy from + // previous passes when seeking to the next output row (rows 4 and 5). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 2, 7, 4), 2, 3); + + // Output row 6. The invalid rect is the entire image because this is + // the end of the third pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 6, 6); + + // Fourth pass. Rows are positioned at 2n + 1. + + // Output row 1. The invalid rect contains the Haeberli rows for this + // output row (just row 1) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 2). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 1, 7, 2), 1, 1); + + // Output row 3. The invalid rect contains the Haeberli rows for this + // output row (just row 3) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 4). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 3, 7, 2), 3, 3); + + // Output row 5. The invalid rect contains the Haeberli rows for this + // output row (just row 5) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 6). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::FINISHED, + OrientedIntRect(0, 5, 7, 2), 5, 5); + + // Assert that we're in the expected final state. + EXPECT_TRUE(aFilter->IsSurfaceFinished()); + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Check that the generated image is correct. As mentioned above, we + // expect two green rows, followed by two red rows, then two green rows, + // etc. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 7; ++row) { + BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5 + ? BGRAColor::Green() + : BGRAColor::Red(); + EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color)); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, + WritePixelsNonProgressiveIntermediateOutput7_7) { + WithDeinterlacingFilter( + IntSize(7, 7), /* aProgressiveDisplay = */ false, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. The output should be a repeating pattern of two green + // rows followed by two red rows but we need to write the rows in the + // order that the deinterlacer expects them. + + // First pass. Output rows are positioned at 8n + 0. + + // Output row 0. The invalid rect is the entire image because this is + // the end of the first pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 0, 0); + + // Second pass. Rows are positioned at 8n + 4. + + // Output row 4. The invalid rect is the entire image because this is + // the end of the second pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 4, 4); + + // Third pass. Rows are positioned at 4n + 2. + + // Output row 2. The invalid rect contains the Haeberli rows for this + // output row (rows 2 and 3) as well as the rows that we copy from + // previous passes when seeking to the next output row (rows 4 and 5). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 2, 7, 4), 2, 2); + + // Output row 6. The invalid rect is the entire image because this is + // the end of the third pass. + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 0, 7, 7), 6, 6); + + // Fourth pass. Rows are positioned at 2n + 1. + + // Output row 1. The invalid rect contains the Haeberli rows for this + // output row (just row 1) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 2). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 1, 7, 2), 1, 1); + + // Output row 3. The invalid rect contains the Haeberli rows for this + // output row (just row 3) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 4). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(), + WriteState::NEED_MORE_DATA, + OrientedIntRect(0, 3, 7, 2), 3, 3); + + // Output row 5. The invalid rect contains the Haeberli rows for this + // output row (just row 5) as well as the rows that we copy from + // previous passes when seeking to the next output row (row 6). + WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(), + WriteState::FINISHED, + OrientedIntRect(0, 5, 7, 2), 5, 5); + + // Assert that we're in the expected final state. + EXPECT_TRUE(aFilter->IsSurfaceFinished()); + Maybe invalidRect = aFilter->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Check that the generated image is correct. As mentioned above, we + // expect two green rows, followed by two red rows, then two green rows, + // etc. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + for (uint32_t row = 0; row < 7; ++row) { + BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5 + ? BGRAColor::Green() + : BGRAColor::Red(); + EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color)); + } + }); +} + +TEST_F(ImageDeinterlacingFilter, DeinterlacingFailsFor0_0) { + // A 0x0 input size is invalid, so configuration should fail. + AssertConfiguringDeinterlacingFilterFails(IntSize(0, 0)); +} + +TEST_F(ImageDeinterlacingFilter, DeinterlacingFailsForMinus1_Minus1) { + // A negative input size is invalid, so configuration should fail. + AssertConfiguringDeinterlacingFilterFails(IntSize(-1, -1)); +} diff --git a/image/test/gtest/TestDownscalingFilter.cpp b/image/test/gtest/TestDownscalingFilter.cpp new file mode 100644 index 0000000000..d00f67d188 --- /dev/null +++ b/image/test/gtest/TestDownscalingFilter.cpp @@ -0,0 +1,231 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +template +void WithDownscalingFilter(const IntSize& aInputSize, + const IntSize& aOutputSize, Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + WithFilterPipeline( + decoder, std::forward(aFunc), + DownscalingConfig{aInputSize, SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false}); +} + +void AssertConfiguringDownscalingFilterFails(const IntSize& aInputSize, + const IntSize& aOutputSize) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AssertConfiguringPipelineFails( + decoder, DownscalingConfig{aInputSize, SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false}); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to99_99) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(99, 99), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 99, 99))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to33_33) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(33, 33), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 33, 33))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to1_1) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(1, 1), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 1, 1))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to33_99) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(33, 99), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 33, 99))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to99_33) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(99, 33), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 99, 33))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to99_1) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(99, 1), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 99, 1))); + }); +} + +TEST(ImageDownscalingFilter, WritePixels100_100to1_99) +{ + WithDownscalingFilter(IntSize(100, 100), IntSize(1, 99), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 1, 99))); + }); +} + +TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to101_101) +{ + // Upscaling is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(101, 101)); +} + +TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to100_100) +{ + // "Scaling" to the same size is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(100, 100)); +} + +TEST(ImageDownscalingFilter, DownscalingFailsFor0_0toMinus1_Minus1) +{ + // A 0x0 input size is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(0, 0), IntSize(-1, -1)); +} + +TEST(ImageDownscalingFilter, DownscalingFailsForMinus1_Minus1toMinus2_Minus2) +{ + // A negative input size is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(-1, -1), IntSize(-2, -2)); +} + +TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to0_0) +{ + // A 0x0 output size is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(0, 0)); +} + +TEST(ImageDownscalingFilter, DownscalingFailsFor100_100toMinus1_Minus1) +{ + // A negative output size is disallowed. + AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(-1, -1)); +} + +TEST(ImageDownscalingFilter, WritePixelsOutput100_100to20_20) +{ + WithDownscalingFilter( + IntSize(100, 100), IntSize(20, 20), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. It consists of 25 lines of green, followed by 25 + // lines of red, followed by 25 lines of green, followed by 25 more + // lines of red. + uint32_t count = 0; + auto result = + aFilter->WritePixels([&]() -> NextPixel { + uint32_t color = + (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100) + ? BGRAColor::Green().AsPixel() + : BGRAColor::Red().AsPixel(); + ++count; + return AsVariant(color); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 100, 100), + IntRect(0, 0, 20, 20)); + + // Check that the generated image is correct. Note that we skip rows + // near the transitions between colors, since the downscaler does not + // produce a sharp boundary at these points. Even some of the rows we + // test need a small amount of fuzz; this is just the nature of Lanczos + // downscaling. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), + /* aFuzz = */ 2)); + EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), + /* aFuzz = */ 3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), + /* aFuzz = */ 3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), + /* aFuzz = */ 3)); + }); +} + +TEST(ImageDownscalingFilter, WritePixelsOutput100_100to10_20) +{ + WithDownscalingFilter( + IntSize(100, 100), IntSize(10, 20), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Fill the image. It consists of 25 lines of green, followed by 25 + // lines of red, followed by 25 lines of green, followed by 25 more + // lines of red. + uint32_t count = 0; + auto result = + aFilter->WritePixels([&]() -> NextPixel { + uint32_t color = + (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100) + ? BGRAColor::Green().AsPixel() + : BGRAColor::Red().AsPixel(); + ++count; + return AsVariant(color); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + AssertCorrectPipelineFinalState(aFilter, IntRect(0, 0, 100, 100), + IntRect(0, 0, 10, 20)); + + // Check that the generated image is correct. Note that we skip rows + // near the transitions between colors, since the downscaler does not + // produce a sharp boundary at these points. Even some of the rows we + // test need a small amount of fuzz; this is just the nature of Lanczos + // downscaling. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), + /* aFuzz = */ 2)); + EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), + /* aFuzz = */ 3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), + /* aFuzz = */ 3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), + /* aFuzz = */ 3)); + }); +} diff --git a/image/test/gtest/TestFrameAnimator.cpp b/image/test/gtest/TestFrameAnimator.cpp new file mode 100644 index 0000000000..cd23f132f8 --- /dev/null +++ b/image/test/gtest/TestFrameAnimator.cpp @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "AnimationSurfaceProvider.h" +#include "Decoder.h" +#include "ImageFactory.h" +#include "nsIInputStream.h" +#include "RasterImage.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +static void CheckFrameAnimatorBlendResults(const ImageTestCase& aTestCase, + RasterImage* aImage, uint8_t aFuzz) { + // Allow the animation to actually begin. + aImage->IncrementAnimationConsumers(); + + // Initialize for the first frame so we can advance. + TimeStamp now = TimeStamp::Now(); + aImage->RequestRefresh(now); + EXPECT_EQ(aImage->GetFrameIndex(imgIContainer::FRAME_CURRENT), 0); + + RefPtr surface = + aImage->GetFrame(imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_NONE); + ASSERT_TRUE(surface != nullptr); + + CheckGeneratedSurface(surface, IntRect(0, 0, 50, 50), + BGRAColor::Transparent(), + aTestCase.ChooseColor(BGRAColor::Red()), aFuzz); + + // Advance to the next/final frame. + now = TimeStamp::Now() + TimeDuration::FromMilliseconds(500); + aImage->RequestRefresh(now); + EXPECT_EQ(aImage->GetFrameIndex(imgIContainer::FRAME_CURRENT), 1); + + surface = + aImage->GetFrame(imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_NONE); + ASSERT_TRUE(surface != nullptr); + CheckGeneratedSurface(surface, IntRect(0, 0, 50, 50), + aTestCase.ChooseColor(BGRAColor::Green()), + aTestCase.ChooseColor(BGRAColor::Red()), aFuzz); +} + +template +static void WithFrameAnimatorDecode(const ImageTestCase& aTestCase, + Func aResultChecker) { + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(aTestCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + NotNull> rasterImage = + WrapNotNull(static_cast(image.get())); + + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into a SourceBuffer. + NotNull> sourceBuffer = WrapNotNull(new SourceBuffer()); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + sourceBuffer->Complete(NS_OK); + + // Create a metadata decoder first, because otherwise RasterImage will get + // unhappy about finding out the image is animated during a full decode. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + DecoderFlags decoderFlags = + DecoderFactory::GetDefaultDecoderFlagsForType(decoderType); + RefPtr task = DecoderFactory::CreateMetadataDecoder( + decoderType, rasterImage, decoderFlags, sourceBuffer); + ASSERT_TRUE(task != nullptr); + + // Run the metadata decoder synchronously. + task->Run(); + task = nullptr; + + // Create an AnimationSurfaceProvider which will manage the decoding process + // and make this decoder's output available in the surface cache. + SurfaceFlags surfaceFlags = aTestCase.mSurfaceFlags; + rv = DecoderFactory::CreateAnimationDecoder( + decoderType, rasterImage, sourceBuffer, aTestCase.mSize, decoderFlags, + surfaceFlags, 0, getter_AddRefs(task)); + EXPECT_EQ(rv, NS_OK); + ASSERT_TRUE(task != nullptr); + + // Run the full decoder synchronously. + task->Run(); + + // Call the lambda to verify the expected results. + aResultChecker(rasterImage.get()); +} + +static void CheckFrameAnimatorBlend(const ImageTestCase& aTestCase, + uint8_t aFuzz = 0) { + WithFrameAnimatorDecode(aTestCase, [&](RasterImage* aImage) { + CheckFrameAnimatorBlendResults(aTestCase, aImage, aFuzz); + }); +} + +class ImageFrameAnimator : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageFrameAnimator, BlendGIFWithFilter) { + CheckFrameAnimatorBlend(BlendAnimatedGIFTestCase()); +} + +TEST_F(ImageFrameAnimator, BlendPNGWithFilter) { + CheckFrameAnimatorBlend(BlendAnimatedPNGTestCase()); +} + +TEST_F(ImageFrameAnimator, BlendWebPWithFilter) { + CheckFrameAnimatorBlend(BlendAnimatedWebPTestCase()); +} + +TEST_F(ImageFrameAnimator, BlendAVIFWithFilter) { + CheckFrameAnimatorBlend(BlendAnimatedAVIFTestCase(), 1); +} diff --git a/image/test/gtest/TestLoader.cpp b/image/test/gtest/TestLoader.cpp new file mode 100644 index 0000000000..d18302f37a --- /dev/null +++ b/image/test/gtest/TestLoader.cpp @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "imgLoader.h" +#include "nsMimeTypes.h" +#include "nsString.h" + +using namespace mozilla; +using namespace mozilla::image; + +static void CheckMimeType(const char* aContents, size_t aLength, + const char* aExpected) { + nsAutoCString detected; + nsresult rv = imgLoader::GetMimeTypeFromContent(aContents, aLength, detected); + if (aExpected) { + ASSERT_NS_SUCCEEDED(rv); + EXPECT_TRUE(detected.EqualsASCII(aExpected)); + } else { + ASSERT_NS_FAILED(rv); + EXPECT_TRUE(detected.IsEmpty()); + } +} + +class ImageLoader : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageLoader, DetectGIF) { + const char buffer[] = "GIF87a"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_GIF); +} + +TEST_F(ImageLoader, DetectPNG) { + const char buffer[] = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_PNG); +} + +TEST_F(ImageLoader, DetectJPEG) { + const char buffer[] = "\xFF\xD8\xFF"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_JPEG); +} + +TEST_F(ImageLoader, DetectART) { + const char buffer[] = "\x4A\x47\xFF\xFF\x00"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_ART); +} + +TEST_F(ImageLoader, DetectBMP) { + const char buffer[] = "BM"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_BMP); +} + +TEST_F(ImageLoader, DetectICO) { + const char buffer[] = "\x00\x00\x01\x00"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_ICO); +} + +TEST_F(ImageLoader, DetectWebP) { + const char buffer[] = "RIFF\xFF\xFF\xFF\xFFWEBPVP8L"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_WEBP); +} + +TEST_F(ImageLoader, DetectAVIFMajorBrand) { + const char buffer[] = + "\x00\x00\x00\x20" // box length + "ftyp" // box type + "avif" // major brand + "\x00\x00\x00\x00" // minor version + "avifmif1miafMA1B"; // compatible brands + CheckMimeType(buffer, sizeof(buffer), IMAGE_AVIF); +} + +TEST_F(ImageLoader, DetectAVIFCompatibleBrand) { + const char buffer[] = + "\x00\x00\x00\x20" // box length + "ftyp" // box type + "XXXX" // major brand + "\x00\x00\x00\x00" // minor version + "avifmif1miafMA1B"; // compatible brands + CheckMimeType(buffer, sizeof(buffer), IMAGE_AVIF); +} + +#ifdef MOZ_JXL +TEST_F(ImageLoader, DetectJXLCodestream) { + const char buffer[] = "\xff\x0a"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_JXL); +} + +TEST_F(ImageLoader, DetectJXLContainer) { + const char buffer[] = + "\x00\x00\x00\x0c" + "JXL " + "\x0d\x0a\x87\x0a"; + CheckMimeType(buffer, sizeof(buffer), IMAGE_JXL); +} +#endif + +TEST_F(ImageLoader, DetectNonImageMP4) { + const char buffer[] = + "\x00\x00\x00\x1c" // box length + "ftyp" // box type + "isom" // major brand + "\x00\x00\x02\x00" // minor version + "isomiso2mp41"; // compatible brands + CheckMimeType(buffer, sizeof(buffer), nullptr); +} + +TEST_F(ImageLoader, DetectNone) { + const char buffer[] = "abcdefghijklmnop"; + CheckMimeType(buffer, sizeof(buffer), nullptr); +} diff --git a/image/test/gtest/TestMetadata.cpp b/image/test/gtest/TestMetadata.cpp new file mode 100644 index 0000000000..d230bf42b9 --- /dev/null +++ b/image/test/gtest/TestMetadata.cpp @@ -0,0 +1,260 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "decoders/nsBMPDecoder.h" +#include "IDecodingTask.h" +#include "imgIContainer.h" +#include "ImageFactory.h" +#include "mozilla/gfx/2D.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "mozilla/RefPtr.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "ProgressTracker.h" +#include "SourceBuffer.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +enum class BMPWithinICO { NO, YES }; + +static void CheckMetadata(const ImageTestCase& aTestCase, + BMPWithinICO aBMPWithinICO = BMPWithinICO::NO) { + nsCOMPtr inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into a SourceBuffer. + auto sourceBuffer = MakeNotNull>(); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_NS_SUCCEEDED(rv); + sourceBuffer->Complete(NS_OK); + + // Create a metadata decoder. + DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); + RefPtr decoder = + DecoderFactory::CreateAnonymousMetadataDecoder(decoderType, sourceBuffer); + ASSERT_TRUE(decoder != nullptr); + RefPtr task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + + if (aBMPWithinICO == BMPWithinICO::YES) { + static_cast(decoder.get())->SetIsWithinICO(); + } + + // Run the metadata decoder synchronously. + task->Run(); + + // Ensure that the metadata decoder didn't make progress it shouldn't have + // (which would indicate that it decoded past the header of the image). + Progress metadataProgress = decoder->TakeProgress(); + EXPECT_TRUE( + 0 == (metadataProgress & + ~(FLAG_SIZE_AVAILABLE | FLAG_HAS_TRANSPARENCY | FLAG_IS_ANIMATED))); + + // If the test case is corrupt, assert what we can and return early. + if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) { + EXPECT_TRUE(decoder->GetDecodeDone()); + EXPECT_TRUE(decoder->HasError()); + return; + } + + EXPECT_TRUE(decoder->GetDecodeDone() && !decoder->HasError()); + + // Check that we got the expected metadata. + EXPECT_TRUE(metadataProgress & FLAG_SIZE_AVAILABLE); + + OrientedIntSize metadataSize = decoder->Size(); + EXPECT_EQ(aTestCase.mSize.width, metadataSize.width); + if (aBMPWithinICO == BMPWithinICO::YES) { + // Half the data is considered to be part of the AND mask if embedded + EXPECT_EQ(aTestCase.mSize.height / 2, metadataSize.height); + } else { + EXPECT_EQ(aTestCase.mSize.height, metadataSize.height); + } + + bool expectTransparency = + aBMPWithinICO == BMPWithinICO::YES + ? true + : bool(aTestCase.mFlags & TEST_CASE_IS_TRANSPARENT); + EXPECT_EQ(expectTransparency, bool(metadataProgress & FLAG_HAS_TRANSPARENCY)); + + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED), + bool(metadataProgress & FLAG_IS_ANIMATED)); + + // Create a full decoder, so we can compare the result. + decoder = DecoderFactory::CreateAnonymousDecoder( + decoderType, sourceBuffer, Nothing(), DecoderFlags::FIRST_FRAME_ONLY, + aTestCase.mSurfaceFlags); + ASSERT_TRUE(decoder != nullptr); + task = + new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false); + + if (aBMPWithinICO == BMPWithinICO::YES) { + static_cast(decoder.get())->SetIsWithinICO(); + } + + // Run the full decoder synchronously. + task->Run(); + + EXPECT_TRUE(decoder->GetDecodeDone() && !decoder->HasError()); + Progress fullProgress = decoder->TakeProgress(); + + // If the metadata decoder set a progress bit, the full decoder should also + // have set the same bit. + EXPECT_EQ(fullProgress, metadataProgress | fullProgress); + + // The full decoder and the metadata decoder should agree on the image's size. + OrientedIntSize fullSize = decoder->Size(); + EXPECT_EQ(metadataSize.width, fullSize.width); + EXPECT_EQ(metadataSize.height, fullSize.height); + + // We should not discover transparency during the full decode that we didn't + // discover during the metadata decode, unless the image is animated. + EXPECT_TRUE(!(fullProgress & FLAG_HAS_TRANSPARENCY) || + (metadataProgress & FLAG_HAS_TRANSPARENCY) || + (fullProgress & FLAG_IS_ANIMATED)); +} + +class ImageDecoderMetadata : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageDecoderMetadata, TransparentAVIF) { + CheckMetadata(TransparentAVIFTestCase()); +} + +TEST_F(ImageDecoderMetadata, PNG) { CheckMetadata(GreenPNGTestCase()); } +TEST_F(ImageDecoderMetadata, TransparentPNG) { + CheckMetadata(TransparentPNGTestCase()); +} +TEST_F(ImageDecoderMetadata, GIF) { CheckMetadata(GreenGIFTestCase()); } +TEST_F(ImageDecoderMetadata, TransparentGIF) { + CheckMetadata(TransparentGIFTestCase()); +} +TEST_F(ImageDecoderMetadata, JPG) { CheckMetadata(GreenJPGTestCase()); } +TEST_F(ImageDecoderMetadata, BMP) { CheckMetadata(GreenBMPTestCase()); } +TEST_F(ImageDecoderMetadata, ICO) { CheckMetadata(GreenICOTestCase()); } +TEST_F(ImageDecoderMetadata, Icon) { CheckMetadata(GreenIconTestCase()); } +TEST_F(ImageDecoderMetadata, WebP) { CheckMetadata(GreenWebPTestCase()); } + +#ifdef MOZ_JXL +TEST_F(ImageDecoderMetadata, JXL) { CheckMetadata(GreenJXLTestCase()); } +TEST_F(ImageDecoderMetadata, TransparentJXL) { + CheckMetadata(TransparentJXLTestCase()); +} +#endif + +TEST_F(ImageDecoderMetadata, AnimatedGIF) { + CheckMetadata(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST_F(ImageDecoderMetadata, AnimatedPNG) { + CheckMetadata(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST_F(ImageDecoderMetadata, FirstFramePaddingGIF) { + CheckMetadata(FirstFramePaddingGIFTestCase()); +} + +TEST_F(ImageDecoderMetadata, TransparentIfWithinICOBMPNotWithinICO) { + CheckMetadata(TransparentIfWithinICOBMPTestCase(TEST_CASE_DEFAULT_FLAGS), + BMPWithinICO::NO); +} + +TEST_F(ImageDecoderMetadata, TransparentIfWithinICOBMPWithinICO) { + CheckMetadata(TransparentIfWithinICOBMPTestCase(TEST_CASE_IS_TRANSPARENT), + BMPWithinICO::YES); +} + +TEST_F(ImageDecoderMetadata, RLE4BMP) { CheckMetadata(RLE4BMPTestCase()); } +TEST_F(ImageDecoderMetadata, RLE8BMP) { CheckMetadata(RLE8BMPTestCase()); } + +TEST_F(ImageDecoderMetadata, Corrupt) { CheckMetadata(CorruptTestCase()); } + +TEST_F(ImageDecoderMetadata, NoFrameDelayGIF) { + CheckMetadata(NoFrameDelayGIFTestCase()); +} + +TEST_F(ImageDecoderMetadata, NoFrameDelayGIFFullDecode) { + ImageTestCase testCase = NoFrameDelayGIFTestCase(); + + // The previous test (NoFrameDelayGIF) verifies that we *don't* detect that + // this test case is animated, because it has a zero frame delay for the first + // frame. This test verifies that when we do a full decode, we detect the + // animation at that point and successfully decode all the frames. + + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(testCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(testCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + ASSERT_NS_SUCCEEDED(rv); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + // Use GetFrame() to force a sync decode of the image. + RefPtr surface = image->GetFrame( + imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_SYNC_DECODE); + + // Ensure that the image's metadata meets our expectations. + IntSize imageSize(0, 0); + rv = image->GetWidth(&imageSize.width); + EXPECT_NS_SUCCEEDED(rv); + rv = image->GetHeight(&imageSize.height); + EXPECT_NS_SUCCEEDED(rv); + + EXPECT_EQ(testCase.mSize.width, imageSize.width); + EXPECT_EQ(testCase.mSize.height, imageSize.height); + + Progress imageProgress = tracker->GetProgress(); + + EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false); + EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true); + + // Ensure that we decoded both frames of the image. + LookupResult result = + SurfaceCache::Lookup(ImageKey(image.get()), + RasterSurfaceKey(imageSize, testCase.mSurfaceFlags, + PlaybackType::eAnimated), + /* aMarkUsed = */ true); + ASSERT_EQ(MatchType::EXACT, result.Type()); + + EXPECT_NS_SUCCEEDED(result.Surface().Seek(0)); + EXPECT_TRUE(bool(result.Surface())); + + RefPtr partialFrame = result.Surface().GetFrame(1); + EXPECT_TRUE(bool(partialFrame)); +} diff --git a/image/test/gtest/TestRemoveFrameRectFilter.cpp b/image/test/gtest/TestRemoveFrameRectFilter.cpp new file mode 100644 index 0000000000..59549f3ddb --- /dev/null +++ b/image/test/gtest/TestRemoveFrameRectFilter.cpp @@ -0,0 +1,311 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +template +void WithRemoveFrameRectFilter(const IntSize& aSize, const IntRect& aFrameRect, + Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + WithFilterPipeline( + decoder, std::forward(aFunc), RemoveFrameRectConfig{aFrameRect}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +void AssertConfiguringRemoveFrameRectFilterFails(const IntSize& aSize, + const IntRect& aFrameRect) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AssertConfiguringPipelineFails( + decoder, RemoveFrameRectConfig{aFrameRect}, + SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false}); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_0_0_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(0, 0, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_0_0_0_0) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(0, 0, 0, 0), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_50_0_0) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(-50, 50, 0, 0), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_Minus50_0_0) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(50, -50, 0, 0), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_150_50_0_0) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(150, 50, 0, 0), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_150_0_0) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(50, 150, 0, 0), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_200_200_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(200, 200, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is zero-size because RemoveFrameRectFilter + // ignores trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus200_25_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(-200, 25, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is zero-size because RemoveFrameRectFilter + // ignores trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_Minus200_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(25, -200, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is zero-size because RemoveFrameRectFilter + // ignores trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_200_25_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(200, 25, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is zero-size because RemoveFrameRectFilter + // ignores trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_200_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(25, 200, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is zero-size because RemoveFrameRectFilter + // ignores trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, + WritePixels100_100_to_Minus200_Minus200_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(-200, -200, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_Minus50_100_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(-50, -50, 100, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 50, 50))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_25_100_50) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(-50, 25, 100, 50), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)), + /* aOutputWriteRect = */ Some(IntRect(0, 25, 50, 50))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_Minus50_50_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(25, -50, 50, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 100)), + /* aOutputWriteRect = */ Some(IntRect(25, 0, 50, 50))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_25_100_50) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(50, 25, 100, 50), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)), + /* aOutputWriteRect = */ Some(IntRect(50, 25, 50, 50))); + }); +} + +TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_50_50_100) +{ + WithRemoveFrameRectFilter( + IntSize(100, 100), IntRect(25, 50, 50, 100), + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + // Note that aInputRect is 50x50 because RemoveFrameRectFilter ignores + // trailing rows that don't show up in the output. (Leading rows + // unfortunately can't be ignored.) + CheckWritePixels( + aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 50)), + /* aOutputWriteRect = */ Some(IntRect(25, 50, 50, 100))); + }); +} + +TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsFor0_0_to_0_0_100_100) +{ + // A zero-size image is disallowed. + AssertConfiguringRemoveFrameRectFilterFails(IntSize(0, 0), + IntRect(0, 0, 100, 100)); +} + +TEST(ImageRemoveFrameRectFilter, + RemoveFrameRectFailsForMinus1_Minus1_to_0_0_100_100) +{ + // A negative-size image is disallowed. + AssertConfiguringRemoveFrameRectFilterFails(IntSize(-1, -1), + IntRect(0, 0, 100, 100)); +} + +TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsFor100_100_to_0_0_0_0) +{ + // A zero size frame rect is disallowed. + AssertConfiguringRemoveFrameRectFilterFails(IntSize(100, 100), + IntRect(0, 0, -1, -1)); +} + +TEST(ImageRemoveFrameRectFilter, + RemoveFrameRectFailsFor100_100_to_0_0_Minus1_Minus1) +{ + // A negative size frame rect is disallowed. + AssertConfiguringRemoveFrameRectFilterFails(IntSize(100, 100), + IntRect(0, 0, -1, -1)); +} diff --git a/image/test/gtest/TestSourceBuffer.cpp b/image/test/gtest/TestSourceBuffer.cpp new file mode 100644 index 0000000000..478ab56610 --- /dev/null +++ b/image/test/gtest/TestSourceBuffer.cpp @@ -0,0 +1,822 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include +#include + +#include "Common.h" +#include "SourceBuffer.h" +#include "SurfaceCache.h" +#include "gtest/gtest.h" +#include "nsIInputStream.h" + +using namespace mozilla; +using namespace mozilla::image; + +using std::min; + +void ExpectChunkAndByteCount(const SourceBufferIterator& aIterator, + uint32_t aChunks, size_t aBytes) { + EXPECT_EQ(aChunks, aIterator.ChunkCount()); + EXPECT_EQ(aBytes, aIterator.ByteCount()); +} + +void ExpectRemainingBytes(const SourceBufferIterator& aIterator, + size_t aBytes) { + EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes)); + EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes + 1)); + + if (aBytes > 0) { + EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(0)); + EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(aBytes - 1)); + } +} + +char GenerateByte(size_t aIndex) { + uint8_t byte = aIndex % 256; + return *reinterpret_cast(&byte); +} + +void GenerateData(char* aOutput, size_t aOffset, size_t aLength) { + for (size_t i = 0; i < aLength; ++i) { + aOutput[i] = GenerateByte(aOffset + i); + } +} + +void GenerateData(char* aOutput, size_t aLength) { + GenerateData(aOutput, 0, aLength); +} + +void CheckData(const char* aData, size_t aOffset, size_t aLength) { + for (size_t i = 0; i < aLength; ++i) { + ASSERT_EQ(GenerateByte(aOffset + i), aData[i]); + } +} + +enum class AdvanceMode { eAdvanceAsMuchAsPossible, eAdvanceByLengthExactly }; + +class ImageSourceBuffer : public ::testing::Test { + public: + ImageSourceBuffer() + : mSourceBuffer(new SourceBuffer), + mExpectNoResume(new ExpectNoResume), + mCountResumes(new CountResumes) { + GenerateData(mData, sizeof(mData)); + EXPECT_FALSE(mSourceBuffer->IsComplete()); + } + + protected: + void CheckedAppendToBuffer(const char* aData, size_t aLength) { + EXPECT_NS_SUCCEEDED(mSourceBuffer->Append(aData, aLength)); + } + + void CheckedAppendToBufferLastByteForLength(size_t aLength) { + const char lastByte = GenerateByte(aLength); + CheckedAppendToBuffer(&lastByte, 1); + } + + void CheckedAppendToBufferInChunks(size_t aChunkLength, size_t aTotalLength) { + char* data = new char[aChunkLength]; + + size_t bytesWritten = 0; + while (bytesWritten < aTotalLength) { + GenerateData(data, bytesWritten, aChunkLength); + size_t toWrite = min(aChunkLength, aTotalLength - bytesWritten); + CheckedAppendToBuffer(data, toWrite); + bytesWritten += toWrite; + } + + delete[] data; + } + + void CheckedCompleteBuffer(nsresult aCompletionStatus = NS_OK) { + mSourceBuffer->Complete(aCompletionStatus); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + } + + void CheckedCompleteBuffer(SourceBufferIterator& aIterator, size_t aLength, + nsresult aCompletionStatus = NS_OK) { + CheckedCompleteBuffer(aCompletionStatus); + ExpectRemainingBytes(aIterator, aLength); + } + + void CheckedAdvanceIteratorStateOnly( + SourceBufferIterator& aIterator, size_t aLength, uint32_t aChunks, + size_t aTotalLength, + AdvanceMode aAdvanceMode = AdvanceMode::eAdvanceAsMuchAsPossible) { + const size_t advanceBy = + aAdvanceMode == AdvanceMode::eAdvanceAsMuchAsPossible ? SIZE_MAX + : aLength; + + auto state = aIterator.AdvanceOrScheduleResume(advanceBy, mExpectNoResume); + ASSERT_EQ(SourceBufferIterator::READY, state); + EXPECT_TRUE(aIterator.Data()); + EXPECT_EQ(aLength, aIterator.Length()); + + ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength); + } + + void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator, + size_t aLength) { + CheckedAdvanceIteratorStateOnly(aIterator, aLength, 1, aLength); + } + + void CheckedAdvanceIterator( + SourceBufferIterator& aIterator, size_t aLength, uint32_t aChunks, + size_t aTotalLength, + AdvanceMode aAdvanceMode = AdvanceMode::eAdvanceAsMuchAsPossible) { + // Check that the iterator is in the expected state. + CheckedAdvanceIteratorStateOnly(aIterator, aLength, aChunks, aTotalLength, + aAdvanceMode); + + // Check that we read the expected data. To do this, we need to compute our + // offset in the SourceBuffer, but fortunately that's pretty easy: it's the + // total number of bytes the iterator has advanced through, minus the length + // of the current chunk. + const size_t offset = aIterator.ByteCount() - aIterator.Length(); + CheckData(aIterator.Data(), offset, aIterator.Length()); + } + + void CheckedAdvanceIterator(SourceBufferIterator& aIterator, size_t aLength) { + CheckedAdvanceIterator(aIterator, aLength, 1, aLength); + } + + void CheckIteratorMustWait(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + auto state = aIterator.AdvanceOrScheduleResume(1, aOnResume); + EXPECT_EQ(SourceBufferIterator::WAITING, state); + } + + void CheckIteratorIsComplete(SourceBufferIterator& aIterator, + uint32_t aChunks, size_t aTotalLength, + nsresult aCompletionStatus = NS_OK) { + ASSERT_TRUE(mSourceBuffer->IsComplete()); + auto state = aIterator.AdvanceOrScheduleResume(1, mExpectNoResume); + ASSERT_EQ(SourceBufferIterator::COMPLETE, state); + EXPECT_EQ(aCompletionStatus, aIterator.CompletionStatus()); + ExpectRemainingBytes(aIterator, 0); + ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength); + } + + void CheckIteratorIsComplete(SourceBufferIterator& aIterator, + size_t aTotalLength) { + CheckIteratorIsComplete(aIterator, 1, aTotalLength); + } + + AutoInitializeImageLib mInit; + char mData[9]; + RefPtr mSourceBuffer; + RefPtr mExpectNoResume; + RefPtr mCountResumes; +}; + +TEST_F(ImageSourceBuffer, InitialState) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // RemainingBytesIsNoMoreThan() should always return false in the initial + // state, since we can't know the answer until Complete() has been called. + EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(0)); + EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(SIZE_MAX)); + + // We haven't advanced our iterator at all, so its counters should be zero. + ExpectChunkAndByteCount(iterator, 0, 0); + + // Attempt to advance; we should fail, and end up in the WAITING state. We + // expect no resumes because we don't actually append anything to the + // SourceBuffer in this test. + CheckIteratorMustWait(iterator, mExpectNoResume); +} + +TEST_F(ImageSourceBuffer, ZeroLengthBufferAlwaysFails) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Complete the buffer without writing to it, providing a successful + // completion status. + CheckedCompleteBuffer(iterator, 0); + + // Completing a buffer without writing to it results in an automatic failure; + // make sure that the actual completion status we get from the iterator + // reflects this. + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE); +} + +TEST_F(ImageSourceBuffer, CompleteSuccess) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1); + + // We should be able to advance once (to read the single byte) and then should + // reach the COMPLETE state with a successful status. + CheckedAdvanceIterator(iterator, 1); + CheckIteratorIsComplete(iterator, 1); +} + +TEST_F(ImageSourceBuffer, CompleteFailure) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1, NS_ERROR_FAILURE); + + // Advance the iterator. Because a failing status is propagated to the + // iterator as soon as it advances, we won't be able to read the single byte + // that we wrote above; we go directly into the COMPLETE state. + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE); +} + +TEST_F(ImageSourceBuffer, Append) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer. + EXPECT_NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))); + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); + + // Verify that we can read it back via the iterator, and that the final state + // is what we expect. + CheckedAdvanceIterator(iterator, sizeof(mData)); + CheckIteratorIsComplete(iterator, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, HugeAppendFails) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // We should fail to append anything bigger than what the SurfaceCache can + // hold, so use the SurfaceCache's maximum capacity to calculate what a + // "massive amount of data" (see below) consists of on this platform. + ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX); + const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1; + + // Attempt to write a massive amount of data and verify that it fails. (We'd + // get a buffer overrun during the test if it succeeds, but if it succeeds + // that's the least of our problems.) + EXPECT_NS_FAILED(mSourceBuffer->Append(mData, hugeSize)); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY); +} + +TEST_F(ImageSourceBuffer, AppendFromInputStream) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Construct an input stream with some arbitrary data. (We use test data from + // one of the decoder tests.) + nsCOMPtr inputStream = LoadFile(GreenPNGTestCase().mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + ASSERT_NS_SUCCEEDED(inputStream->Available(&length)); + + // Write test data to the buffer. + EXPECT_TRUE( + NS_SUCCEEDED(mSourceBuffer->AppendFromInputStream(inputStream, length))); + CheckedCompleteBuffer(iterator, length); + + // Verify that the iterator sees the appropriate amount of data. + CheckedAdvanceIteratorStateOnly(iterator, length); + CheckIteratorIsComplete(iterator, length); +} + +TEST_F(ImageSourceBuffer, AppendAfterComplete) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer. + EXPECT_NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))); + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); + + // Verify that we can read it back via the iterator, and that the final state + // is what we expect. + CheckedAdvanceIterator(iterator, sizeof(mData)); + CheckIteratorIsComplete(iterator, sizeof(mData)); + + // Write more data to the completed buffer. + EXPECT_NS_FAILED(mSourceBuffer->Append(mData, sizeof(mData))); + + // Try to read with a new iterator and verify that the new data got ignored. + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); + CheckedAdvanceIterator(iterator2, sizeof(mData)); + CheckIteratorIsComplete(iterator2, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, MinChunkCapacity) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer using many small appends. Since + // ExpectLength() isn't being called, we should be able to write up to + // SourceBuffer::MIN_CHUNK_CAPACITY bytes without a second chunk being + // allocated. + CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees the appropriate amount of data. + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(SourceBuffer::MIN_CHUNK_CAPACITY); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1); + CheckIteratorIsComplete(iterator, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1); +} + +TEST_F(ImageSourceBuffer, ExpectLengthAllocatesRequestedCapacity) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the buffer, + // but call ExpectLength() first to make SourceBuffer expect only a single + // byte. We expect this to still result in two chunks, because we trust the + // initial guess of ExpectLength() but after that it will only allocate chunks + // of at least MIN_CHUNK_CAPACITY bytes. + EXPECT_NS_SUCCEEDED(mSourceBuffer->ExpectLength(1)); + CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY); + CheckedCompleteBuffer(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees a first chunk with 1 byte, and a second chunk + // with the remaining data. + CheckedAdvanceIterator(iterator, 1, 1, 1); + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY - 1, 2, + SourceBuffer::MIN_CHUNK_CAPACITY); + CheckIteratorIsComplete(iterator, 2, SourceBuffer::MIN_CHUNK_CAPACITY); +} + +TEST_F(ImageSourceBuffer, ExpectLengthGrowsAboveMinCapacity) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer, calling ExpectLength() with the correct length first. We expect + // this to result in only one chunk, because ExpectLength() allows us to + // allocate a larger first chunk than MIN_CHUNK_CAPACITY bytes. + const size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY; + EXPECT_NS_SUCCEEDED(mSourceBuffer->ExpectLength(length)); + CheckedAppendToBufferInChunks(10, length); + + // Verify that the iterator sees a single chunk. + CheckedAdvanceIterator(iterator, length); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(length); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, length + 1); + CheckIteratorIsComplete(iterator, 2, length + 1); +} + +TEST_F(ImageSourceBuffer, HugeExpectLengthFails) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // ExpectLength() should fail if the length is bigger than what the + // SurfaceCache can hold, so use the SurfaceCache's maximum capacity to + // calculate what a "massive amount of data" (see below) consists of on this + // platform. + ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX); + const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1; + + // Attempt to write a massive amount of data and verify that it fails. (We'd + // get a buffer overrun during the test if it succeeds, but if it succeeds + // that's the least of our problems.) + EXPECT_NS_FAILED(mSourceBuffer->ExpectLength(hugeSize)); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_INVALID_ARG); +} + +TEST_F(ImageSourceBuffer, LargeAppendsAllocateOnlyOneChunk) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. We expect this to result in only one + // chunk even though ExpectLength() wasn't called, because we should always + // allocate a new chunk large enough to store the data we have at hand. + constexpr size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY; + char data[length]; + GenerateData(data, sizeof(data)); + CheckedAppendToBuffer(data, length); + + // Verify that the iterator sees a single chunk. + CheckedAdvanceIterator(iterator, length); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(length); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, length + 1); + CheckIteratorIsComplete(iterator, 2, length + 1); +} + +TEST_F(ImageSourceBuffer, LargeAppendsAllocateAtMostOneChunk) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Allocate some data we'll use below. + constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2; + constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = firstWriteLength + secondWriteLength; + char data[totalLength]; + GenerateData(data, sizeof(data)); + + // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. This should fill half of the first chunk. + CheckedAppendToBuffer(data, firstWriteLength); + + // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to + // the buffer in a single Append() call. We expect this to result in the first + // of the first chunk being filled and a new chunk being allocated for the + // remainder. + CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength); + + // Verify that the iterator sees a MIN_CHUNK_CAPACITY-length chunk. + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees a second chunk of the length we expect. + const size_t expectedSecondChunkLength = + totalLength - SourceBuffer::MIN_CHUNK_CAPACITY; + CheckedAdvanceIterator(iterator, expectedSecondChunkLength, 2, totalLength); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(totalLength); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 3, totalLength + 1); + CheckIteratorIsComplete(iterator, 3, totalLength + 1); +} + +TEST_F(ImageSourceBuffer, OversizedAppendsAllocateAtMostOneChunk) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Allocate some data we'll use below. + constexpr size_t writeLength = SourceBuffer::MAX_CHUNK_CAPACITY + 1; + + // Write SourceBuffer::MAX_CHUNK_CAPACITY + 1 bytes of test data to the + // buffer in a single Append() call. This should cause one chunk to be + // allocated because we wrote it as a single block. + CheckedAppendToBufferInChunks(writeLength, writeLength); + + // Verify that the iterator sees a MAX_CHUNK_CAPACITY+1-length chunk. + CheckedAdvanceIterator(iterator, writeLength); + + CheckedCompleteBuffer(NS_OK); + CheckIteratorIsComplete(iterator, 1, writeLength); +} + +TEST_F(ImageSourceBuffer, CompactionHappensWhenBufferIsComplete) { + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. + CheckedAppendToBufferInChunks(chunkLength, totalLength); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that the iterator sees two chunks. + CheckedAdvanceIterator(iterator, chunkLength); + CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength); + } + + // Complete the buffer, which should trigger compaction implicitly. + CheckedCompleteBuffer(); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that compaction happened and there's now only one chunk. + CheckedAdvanceIterator(iterator, totalLength); + CheckIteratorIsComplete(iterator, 1, totalLength); + } +} + +TEST_F(ImageSourceBuffer, CompactionIsDelayedWhileIteratorsExist) { + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + { + SourceBufferIterator outerIterator = mSourceBuffer->Iterator(); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write enough data to create two chunks. + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Verify that the iterator sees two chunks. Since there are live + // iterators, compaction shouldn't have happened when we completed the + // buffer. + CheckedAdvanceIterator(iterator, chunkLength); + CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength); + CheckIteratorIsComplete(iterator, 2, totalLength); + } + + // Now |iterator| has been destroyed, but |outerIterator| still exists, so + // we expect no compaction to have occurred at this point. + CheckedAdvanceIterator(outerIterator, chunkLength); + CheckedAdvanceIterator(outerIterator, chunkLength, 2, totalLength); + CheckIteratorIsComplete(outerIterator, 2, totalLength); + } + + // Now all iterators have been destroyed. Since the buffer was already + // complete, we expect compaction to happen implicitly here. + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that compaction happened and there's now only one chunk. + CheckedAdvanceIterator(iterator, totalLength); + CheckIteratorIsComplete(iterator, 1, totalLength); + } +} + +TEST_F(ImageSourceBuffer, SourceBufferIteratorsCanBeMoved) { + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create an iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + auto GetIterator = [&] { + SourceBufferIterator lambdaIterator = mSourceBuffer->Iterator(); + CheckedAdvanceIterator(lambdaIterator, chunkLength); + return lambdaIterator; + }; + + // Move-construct |movedIterator| from the iterator returned from + // GetIterator() and check that its state is as we expect. + SourceBufferIterator tmpIterator = GetIterator(); + SourceBufferIterator movedIterator(std::move(tmpIterator)); + EXPECT_TRUE(movedIterator.Data()); + EXPECT_EQ(chunkLength, movedIterator.Length()); + ExpectChunkAndByteCount(movedIterator, 1, chunkLength); + + // Make sure that we can advance the iterator. + CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength); + + // Make sure that the iterator handles completion properly. + CheckIteratorIsComplete(movedIterator, 2, totalLength); + + // Move-assign |movedIterator| from the iterator returned from + // GetIterator() and check that its state is as we expect. + tmpIterator = GetIterator(); + movedIterator = std::move(tmpIterator); + EXPECT_TRUE(movedIterator.Data()); + EXPECT_EQ(chunkLength, movedIterator.Length()); + ExpectChunkAndByteCount(movedIterator, 1, chunkLength); + + // Make sure that we can advance the iterator. + CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength); + + // Make sure that the iterator handles completion properly. + CheckIteratorIsComplete(movedIterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkAdvance) { + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create our iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Advance through the first chunk. The chunk count should not increase. + // We check that by always passing 1 for the |aChunks| parameter of + // CheckedAdvanceIteratorStateOnly(). We have to call CheckData() manually + // because the offset calculation in CheckedAdvanceIterator() assumes that + // we're advancing a chunk at a time. + size_t offset = 0; + while (offset < chunkLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + } + + // Read the first byte of the second chunk. This is the point at which we + // can't advance within the same chunk, so the chunk count should increase. We + // check that by passing 2 for the |aChunks| parameter of + // CheckedAdvanceIteratorStateOnly(). + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + + // Read the rest of the second chunk. The chunk count should not increase. + while (offset < totalLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + } + + // Make sure we reached the end. + CheckIteratorIsComplete(iterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvance) { + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create our iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Make an initial zero-length advance. Although a zero-length advance + // normally won't cause us to read a chunk from the SourceBuffer, we'll do so + // if the iterator is in the initial state to keep the invariant that + // SourceBufferIterator in the READY state always returns a non-null pointer + // from Data(). + CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + + // Advance through the first chunk. As in the |SubchunkAdvance| test, the + // chunk count should not increase. We do a zero-length advance after each + // normal advance to ensure that zero-length advances do not change the + // iterator's position or cause a new chunk to be read. + size_t offset = 0; + while (offset < chunkLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + } + + // Read the first byte of the second chunk. This is the point at which we + // can't advance within the same chunk, so the chunk count should increase. As + // before, we do a zero-length advance afterward. + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + + // Read the rest of the second chunk. The chunk count should not increase. As + // before, we do a zero-length advance after each normal advance. + while (offset < totalLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + } + + // Make sure we reached the end. + CheckIteratorIsComplete(iterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvanceWithNoData) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that advancing by zero bytes still makes us enter the WAITING state. + // This is because if we entered the READY state before reading any data at + // all, we'd break the invariant that SourceBufferIterator::Data() always + // returns a non-null pointer in the READY state. + auto state = iterator.AdvanceOrScheduleResume(0, mCountResumes); + EXPECT_EQ(SourceBufferIterator::WAITING, state); + + // Call Complete(). This should trigger a resume. + CheckedCompleteBuffer(); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, NullIResumable) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, nullptr); + + // Append to the buffer, which would cause a resume if we had passed a + // non-null IResumable. + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, AppendTriggersResume) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Call Append(). This should trigger a resume. + mSourceBuffer->Append(mData, sizeof(mData)); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, OnlyOneResumeTriggeredPerAppend) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Allocate some data we'll use below. + constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2; + constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = firstWriteLength + secondWriteLength; + char data[totalLength]; + GenerateData(data, sizeof(data)); + + // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. This should fill half of the first chunk. + // This should trigger a resume. + CheckedAppendToBuffer(data, firstWriteLength); + EXPECT_EQ(1u, mCountResumes->Count()); + + // Advance past the new data and wait again. + CheckedAdvanceIterator(iterator, firstWriteLength); + CheckIteratorMustWait(iterator, mCountResumes); + + // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to + // the buffer in a single Append() call. We expect this to result in the first + // of the first chunk being filled and a new chunk being allocated for the + // remainder. Even though two chunks are getting written to here, only *one* + // resume should get triggered, for a total of two in this test. + CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength); + EXPECT_EQ(2u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, CompleteTriggersResume) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Call Complete(). This should trigger a resume. + CheckedCompleteBuffer(); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, ExpectLengthDoesNotTriggerResume) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mExpectNoResume); + + // Call ExpectLength(). If this triggers a resume, |mExpectNoResume| will + // ensure that the test fails. + mSourceBuffer->ExpectLength(1000); +} + +TEST_F(ImageSourceBuffer, CompleteSuccessWithSameReadLength) { + SourceBufferIterator iterator = mSourceBuffer->Iterator(1); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1); + + // We should be able to advance once (to read the single byte) and then should + // reach the COMPLETE state with a successful status. + CheckedAdvanceIterator(iterator, 1); + CheckIteratorIsComplete(iterator, 1); +} + +TEST_F(ImageSourceBuffer, CompleteSuccessWithSmallerReadLength) { + // Create an iterator limited to one byte. + SourceBufferIterator iterator = mSourceBuffer->Iterator(1); + + // Write two bytes to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 2); + CheckedCompleteBuffer(iterator, 2); + + // We should be able to advance once (to read the single byte) and then should + // reach the COMPLETE state with a successful status, because our iterator is + // limited to a single byte, rather than the full length. + CheckedAdvanceIterator(iterator, 1); + CheckIteratorIsComplete(iterator, 1); +} + +TEST_F(ImageSourceBuffer, CompleteSuccessWithGreaterReadLength) { + // Create an iterator limited to one byte. + SourceBufferIterator iterator = mSourceBuffer->Iterator(2); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1); + + // We should be able to advance once (to read the single byte) and then should + // reach the COMPLETE state with a successful status. Our iterator lets us + // read more but the underlying buffer has been completed. + CheckedAdvanceIterator(iterator, 1); + CheckIteratorIsComplete(iterator, 1); +} diff --git a/image/test/gtest/TestStreamingLexer.cpp b/image/test/gtest/TestStreamingLexer.cpp new file mode 100644 index 0000000000..c83569a7b9 --- /dev/null +++ b/image/test/gtest/TestStreamingLexer.cpp @@ -0,0 +1,935 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "mozilla/Vector.h" +#include "StreamingLexer.h" + +using namespace mozilla; +using namespace mozilla::image; + +enum class TestState { + ONE, + TWO, + THREE, + UNBUFFERED, + TRUNCATED_SUCCESS, + TRUNCATED_FAILURE +}; + +void CheckLexedData(const char* aData, size_t aLength, size_t aOffset, + size_t aExpectedLength) { + EXPECT_TRUE(aLength == aExpectedLength); + + for (size_t i = 0; i < aLength; ++i) { + EXPECT_EQ(aData[i], char(aOffset + i + 1)); + } +} + +LexerTransition DoLex(TestState aState, const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::To(TestState::TWO, 3); + case TestState::TWO: + CheckLexedData(aData, aLength, 3, 3); + return Transition::To(TestState::THREE, 3); + case TestState::THREE: + CheckLexedData(aData, aLength, 6, 3); + return Transition::TerminateSuccess(); + case TestState::TRUNCATED_SUCCESS: + return Transition::TerminateSuccess(); + case TestState::TRUNCATED_FAILURE: + return Transition::TerminateFailure(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithUnbuffered( + TestState aState, const char* aData, size_t aLength, + Vector& aUnbufferedVector) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3); + case TestState::TWO: + CheckLexedData(aUnbufferedVector.begin(), aUnbufferedVector.length(), 3, + 3); + return Transition::To(TestState::THREE, 3); + case TestState::THREE: + CheckLexedData(aData, aLength, 6, 3); + return Transition::TerminateSuccess(); + case TestState::UNBUFFERED: + EXPECT_TRUE(aLength <= 3); + EXPECT_TRUE(aUnbufferedVector.append(aData, aLength)); + return Transition::ContinueUnbuffered(TestState::UNBUFFERED); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithUnbufferedTerminate(TestState aState, + const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3); + case TestState::UNBUFFERED: + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithYield(TestState aState, const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::ToAfterYield(TestState::TWO); + case TestState::TWO: + CheckLexedData(aData, aLength, 0, 3); + return Transition::To(TestState::THREE, 6); + case TestState::THREE: + CheckLexedData(aData, aLength, 3, 6); + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithTerminateAfterYield(TestState aState, + const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::ToAfterYield(TestState::TWO); + case TestState::TWO: + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithZeroLengthStates(TestState aState, + const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::TWO, 0); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 9); + case TestState::THREE: + CheckLexedData(aData, aLength, 0, 9); + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithZeroLengthStatesAtEnd(TestState aState, + const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 9); + return Transition::To(TestState::TWO, 0); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 0); + case TestState::THREE: + EXPECT_TRUE(aLength == 0); + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithZeroLengthYield(TestState aState, + const char* aData, + size_t aLength) { + switch (aState) { + case TestState::ONE: + EXPECT_EQ(0u, aLength); + return Transition::ToAfterYield(TestState::TWO); + case TestState::TWO: + EXPECT_EQ(0u, aLength); + return Transition::To(TestState::THREE, 9); + case TestState::THREE: + CheckLexedData(aData, aLength, 0, 9); + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithZeroLengthStatesUnbuffered( + TestState aState, const char* aData, size_t aLength) { + switch (aState) { + case TestState::ONE: + EXPECT_TRUE(aLength == 0); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 0); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 9); + case TestState::THREE: + CheckLexedData(aData, aLength, 0, 9); + return Transition::TerminateSuccess(); + case TestState::UNBUFFERED: + ADD_FAILURE() << "Should not enter zero-length unbuffered state"; + return Transition::TerminateFailure(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition DoLexWithZeroLengthStatesAfterUnbuffered( + TestState aState, const char* aData, size_t aLength) { + switch (aState) { + case TestState::ONE: + EXPECT_TRUE(aLength == 0); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 9); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 0); + case TestState::THREE: + EXPECT_TRUE(aLength == 0); + return Transition::TerminateSuccess(); + case TestState::UNBUFFERED: + CheckLexedData(aData, aLength, 0, 9); + return Transition::ContinueUnbuffered(TestState::UNBUFFERED); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +class ImageStreamingLexer : public ::testing::Test { + public: + // Note that mLexer is configured to enter TerminalState::FAILURE immediately + // if the input data is truncated. We don't expect that to happen in most + // tests, so we want to detect that issue. If a test needs a different + // behavior, we create a special StreamingLexer just for that test. + ImageStreamingLexer() + : mLexer(Transition::To(TestState::ONE, 3), + Transition::TerminateFailure()), + mSourceBuffer(new SourceBuffer), + mIterator(mSourceBuffer->Iterator()), + mExpectNoResume(new ExpectNoResume), + mCountResumes(new CountResumes) {} + + protected: + void CheckTruncatedState(StreamingLexer& aLexer, + TerminalState aExpectedTerminalState, + nsresult aCompletionStatus = NS_OK) { + for (unsigned i = 0; i < 9; ++i) { + if (i < 2) { + mSourceBuffer->Append(mData + i, 1); + } else if (i == 2) { + mSourceBuffer->Complete(aCompletionStatus); + } + + LexerResult result = aLexer.Lex(mIterator, mCountResumes, DoLex); + + if (i >= 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(aExpectedTerminalState, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); + } + + AutoInitializeImageLib mInit; + const char mData[9]{1, 2, 3, 4, 5, 6, 7, 8, 9}; + StreamingLexer mLexer; + RefPtr mSourceBuffer; + SourceBufferIterator mIterator; + RefPtr mExpectNoResume; + RefPtr mCountResumes; +}; + +TEST_F(ImageStreamingLexer, ZeroLengthData) { + // Test a zero-length input. + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::FAILURE, result.as()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthDataUnbuffered) { + // Test a zero-length input. + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be unbuffered. + StreamingLexer lexer( + Transition::ToUnbuffered(TestState::ONE, TestState::UNBUFFERED, + sizeof(mData)), + Transition::TerminateFailure()); + + LexerResult result = lexer.Lex(mIterator, mExpectNoResume, DoLex); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::FAILURE, result.as()); +} + +TEST_F(ImageStreamingLexer, StartWithTerminal) { + // Create a special StreamingLexer for this test because we want the first + // state to be a terminal state. This doesn't really make sense, but we should + // handle it. + StreamingLexer lexer(Transition::TerminateSuccess(), + Transition::TerminateFailure()); + LexerResult result = lexer.Lex(mIterator, mExpectNoResume, DoLex); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, SingleChunk) { + // Test delivering all the data at once. + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, SingleChunkWithUnbuffered) { + Vector unbufferedVector; + + // Test delivering all the data at once. + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex( + mIterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { + return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); + }); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, SingleChunkWithYield) { + // Test delivering all the data at once. + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLexWithYield); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + + result = mLexer.Lex(mIterator, mExpectNoResume, DoLexWithYield); + ASSERT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, ChunkPerState) { + // Test delivering in perfectly-sized chunks, one per state. + for (unsigned i = 0; i < 3; ++i) { + mSourceBuffer->Append(mData + 3 * i, 3); + LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLex); + + if (i == 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, ChunkPerStateWithUnbuffered) { + Vector unbufferedVector; + + // Test delivering in perfectly-sized chunks, one per state. + for (unsigned i = 0; i < 3; ++i) { + mSourceBuffer->Append(mData + 3 * i, 3); + LexerResult result = mLexer.Lex( + mIterator, mCountResumes, + [&](TestState aState, const char* aData, size_t aLength) { + return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); + }); + + if (i == 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, ChunkPerStateWithYield) { + // Test delivering in perfectly-sized chunks, one per state. + mSourceBuffer->Append(mData, 3); + LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield); + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + + result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield); + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + + mSourceBuffer->Append(mData + 3, 6); + result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + + EXPECT_EQ(1u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, ChunkPerStateWithUnbufferedYield) { + size_t unbufferedCallCount = 0; + Vector unbufferedVector; + auto lexerFunc = [&](TestState aState, const char* aData, + size_t aLength) -> LexerTransition { + switch (aState) { + case TestState::ONE: + CheckLexedData(aData, aLength, 0, 3); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, + 3); + case TestState::TWO: + CheckLexedData(unbufferedVector.begin(), unbufferedVector.length(), 3, + 3); + return Transition::To(TestState::THREE, 3); + case TestState::THREE: + CheckLexedData(aData, aLength, 6, 3); + return Transition::TerminateSuccess(); + case TestState::UNBUFFERED: + switch (unbufferedCallCount) { + case 0: + CheckLexedData(aData, aLength, 3, 3); + EXPECT_TRUE(unbufferedVector.append(aData, 2)); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 2 bytes. + return Transition::ContinueUnbufferedAfterYield( + TestState::UNBUFFERED, 2); + + case 1: + CheckLexedData(aData, aLength, 5, 1); + EXPECT_TRUE(unbufferedVector.append(aData, 1)); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 1 byte. + // We should end up in the TWO state. + return Transition::ContinueUnbuffered(TestState::UNBUFFERED); + } + ADD_FAILURE() << "Too many invocations of TestState::UNBUFFERED"; + return Transition::TerminateFailure(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } + }; + + // Test delivering in perfectly-sized chunks, one per state. + for (unsigned i = 0; i < 3; ++i) { + mSourceBuffer->Append(mData + 3 * i, 3); + LexerResult result = mLexer.Lex(mIterator, mCountResumes, lexerFunc); + + switch (i) { + case 0: + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + EXPECT_EQ(0u, unbufferedCallCount); + break; + + case 1: + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + EXPECT_EQ(1u, unbufferedCallCount); + + result = mLexer.Lex(mIterator, mCountResumes, lexerFunc); + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + EXPECT_EQ(2u, unbufferedCallCount); + break; + + case 2: + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + break; + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex(mIterator, mCountResumes, lexerFunc); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, OneByteChunks) { + // Test delivering in one byte chunks. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLex); + + if (i == 8) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(8u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, OneByteChunksWithUnbuffered) { + Vector unbufferedVector; + + // Test delivering in one byte chunks. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = mLexer.Lex( + mIterator, mCountResumes, + [&](TestState aState, const char* aData, size_t aLength) { + return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); + }); + + if (i == 8) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(8u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, OneByteChunksWithYield) { + // Test delivering in one byte chunks. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield); + + switch (i) { + case 2: + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + + result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield); + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + break; + + case 8: + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + break; + + default: + EXPECT_TRUE(i < 9); + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + EXPECT_EQ(8u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, ZeroLengthState) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be zero length. + StreamingLexer lexer(Transition::To(TestState::ONE, 0), + Transition::TerminateFailure()); + + LexerResult result = + lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStates); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthStatesAtEnd) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to consume the full input. + StreamingLexer lexer(Transition::To(TestState::ONE, 9), + Transition::TerminateFailure()); + + LexerResult result = + lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesAtEnd); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthStateWithYield) { + // Create a special StreamingLexer for this test because we want the first + // state to be zero length. + StreamingLexer lexer(Transition::To(TestState::ONE, 0), + Transition::TerminateFailure()); + + mSourceBuffer->Append(mData, 3); + LexerResult result = + lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthYield); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + + result = lexer.Lex(mIterator, mCountResumes, DoLexWithZeroLengthYield); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + + mSourceBuffer->Append(mData + 3, sizeof(mData) - 3); + mSourceBuffer->Complete(NS_OK); + result = lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthYield); + ASSERT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbuffered) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be both zero length and unbuffered. + StreamingLexer lexer( + Transition::ToUnbuffered(TestState::ONE, TestState::UNBUFFERED, 0), + Transition::TerminateFailure()); + + LexerResult result = lexer.Lex(mIterator, mExpectNoResume, + DoLexWithZeroLengthStatesUnbuffered); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthStateAfterUnbuffered) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be zero length. + StreamingLexer lexer(Transition::To(TestState::ONE, 0), + Transition::TerminateFailure()); + + LexerResult result = lexer.Lex(mIterator, mExpectNoResume, + DoLexWithZeroLengthStatesAfterUnbuffered); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbufferedYield) { + size_t unbufferedCallCount = 0; + auto lexerFunc = [&](TestState aState, const char* aData, + size_t aLength) -> LexerTransition { + switch (aState) { + case TestState::ONE: + EXPECT_EQ(0u, aLength); + return Transition::TerminateSuccess(); + + case TestState::UNBUFFERED: + switch (unbufferedCallCount) { + case 0: + CheckLexedData(aData, aLength, 0, 3); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 0 bytes. + return Transition::ContinueUnbufferedAfterYield( + TestState::UNBUFFERED, 0); + + case 1: + CheckLexedData(aData, aLength, 0, 3); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 2 bytes. + return Transition::ContinueUnbufferedAfterYield( + TestState::UNBUFFERED, 2); + + case 2: + EXPECT_EQ(1u, aLength); + CheckLexedData(aData, aLength, 2, 1); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 1 bytes. + return Transition::ContinueUnbufferedAfterYield( + TestState::UNBUFFERED, 1); + + case 3: + CheckLexedData(aData, aLength, 3, 6); + unbufferedCallCount++; + + // Continue after yield, telling StreamingLexer we consumed 6 bytes. + // We should transition to TestState::ONE when we return from the + // yield. + return Transition::ContinueUnbufferedAfterYield( + TestState::UNBUFFERED, 6); + } + + ADD_FAILURE() << "Too many invocations of TestState::UNBUFFERED"; + return Transition::TerminateFailure(); + + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } + }; + + // Create a special StreamingLexer for this test because we want the first + // state to be unbuffered. + StreamingLexer lexer( + Transition::ToUnbuffered(TestState::ONE, TestState::UNBUFFERED, + sizeof(mData)), + Transition::TerminateFailure()); + + mSourceBuffer->Append(mData, 3); + LexerResult result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + EXPECT_EQ(1u, unbufferedCallCount); + + result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + EXPECT_EQ(2u, unbufferedCallCount); + + result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + EXPECT_EQ(3u, unbufferedCallCount); + + result = lexer.Lex(mIterator, mCountResumes, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + EXPECT_EQ(3u, unbufferedCallCount); + + mSourceBuffer->Append(mData + 3, 6); + mSourceBuffer->Complete(NS_OK); + EXPECT_EQ(1u, mCountResumes->Count()); + result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + EXPECT_EQ(4u, unbufferedCallCount); + + result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc); + ASSERT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, TerminateSuccess) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Test that Terminate is "sticky". + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + LexerResult result = + mLexer.Lex(iterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { + EXPECT_TRUE(aState == TestState::ONE); + return Transition::TerminateSuccess(); + }); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); + result = mLexer.Lex(iterator2, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { + EXPECT_TRUE(false); // Shouldn't get here. + return Transition::TerminateFailure(); + }); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); +} + +TEST_F(ImageStreamingLexer, TerminateFailure) { + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Test that Terminate is "sticky". + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + LexerResult result = + mLexer.Lex(iterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { + EXPECT_TRUE(aState == TestState::ONE); + return Transition::TerminateFailure(); + }); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::FAILURE, result.as()); + + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); + result = mLexer.Lex(iterator2, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { + EXPECT_TRUE(false); // Shouldn't get here. + return Transition::TerminateFailure(); + }); + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::FAILURE, result.as()); +} + +TEST_F(ImageStreamingLexer, TerminateUnbuffered) { + // Test that Terminate works during an unbuffered read. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = + mLexer.Lex(mIterator, mCountResumes, DoLexWithUnbufferedTerminate); + + if (i > 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + // We expect 3 resumes because TestState::ONE consumes 3 bytes and then + // transitions to TestState::UNBUFFERED, which calls TerminateSuccess() as + // soon as it receives a single byte. That's four bytes total, which are + // delivered one at a time, requiring 3 resumes. + EXPECT_EQ(3u, mCountResumes->Count()); + + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, TerminateAfterYield) { + // Test that Terminate works after yielding. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = + mLexer.Lex(mIterator, mCountResumes, DoLexWithTerminateAfterYield); + + if (i > 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else if (i == 2) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + // We expect 2 resumes because TestState::ONE consumes 3 bytes and then + // yields. When the lexer resumes at TestState::TWO, which receives the same 3 + // bytes, TerminateSuccess() gets called immediately. That's three bytes + // total, which are delivered one at a time, requiring 2 resumes. + EXPECT_EQ(2u, mCountResumes->Count()); + + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, SourceBufferImmediateComplete) { + // Test calling SourceBuffer::Complete() without appending any data. This + // causes the SourceBuffer to automatically have a failing completion status, + // no matter what you pass, so we expect TerminalState::FAILURE below. + mSourceBuffer->Complete(NS_OK); + + LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::FAILURE, result.as()); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedTerminalStateSuccess) { + // Test that using a terminal state (in this case TerminalState::SUCCESS) as a + // truncated state works. + StreamingLexer lexer(Transition::To(TestState::ONE, 3), + Transition::TerminateSuccess()); + + CheckTruncatedState(lexer, TerminalState::SUCCESS); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedTerminalStateFailure) { + // Test that using a terminal state (in this case TerminalState::FAILURE) as a + // truncated state works. + StreamingLexer lexer(Transition::To(TestState::ONE, 3), + Transition::TerminateFailure()); + + CheckTruncatedState(lexer, TerminalState::FAILURE); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedStateReturningSuccess) { + // Test that a truncated state that returns TerminalState::SUCCESS works. When + // |lexer| discovers that the data is truncated, it invokes the + // TRUNCATED_SUCCESS state, which returns TerminalState::SUCCESS. + // CheckTruncatedState() verifies that this happens. + StreamingLexer lexer( + Transition::To(TestState::ONE, 3), + Transition::To(TestState::TRUNCATED_SUCCESS, 0)); + + CheckTruncatedState(lexer, TerminalState::SUCCESS); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedStateReturningFailure) { + // Test that a truncated state that returns TerminalState::FAILURE works. When + // |lexer| discovers that the data is truncated, it invokes the + // TRUNCATED_FAILURE state, which returns TerminalState::FAILURE. + // CheckTruncatedState() verifies that this happens. + StreamingLexer lexer( + Transition::To(TestState::ONE, 3), + Transition::To(TestState::TRUNCATED_FAILURE, 0)); + + CheckTruncatedState(lexer, TerminalState::FAILURE); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedFailingCompleteStatus) { + // Test that calling SourceBuffer::Complete() with a failing status results in + // an immediate TerminalState::FAILURE result. (Note that |lexer|'s truncated + // state is TerminalState::SUCCESS, so if we ignore the failing status, the + // test will fail.) + StreamingLexer lexer(Transition::To(TestState::ONE, 3), + Transition::TerminateSuccess()); + + CheckTruncatedState(lexer, TerminalState::FAILURE, NS_ERROR_FAILURE); +} + +TEST_F(ImageStreamingLexer, NoSourceBufferResumable) { + // Test delivering in one byte chunks with no IResumable. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + LexerResult result = mLexer.Lex(mIterator, nullptr, DoLex); + + if (i == 8) { + EXPECT_TRUE(result.is()); + EXPECT_EQ(TerminalState::SUCCESS, result.as()); + } else { + EXPECT_TRUE(result.is()); + EXPECT_EQ(Yield::NEED_MORE_DATA, result.as()); + } + } + + mSourceBuffer->Complete(NS_OK); +} diff --git a/image/test/gtest/TestSurfaceCache.cpp b/image/test/gtest/TestSurfaceCache.cpp new file mode 100644 index 0000000000..53193b1a88 --- /dev/null +++ b/image/test/gtest/TestSurfaceCache.cpp @@ -0,0 +1,159 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "Common.h" +#include "imgIContainer.h" +#include "ImageFactory.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPrefs_image.h" +#include "nsIInputStream.h" +#include "nsString.h" +#include "ProgressTracker.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +class ImageSurfaceCache : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageSurfaceCache, Factor2) { + ImageTestCase testCase = GreenPNGTestCase(); + + // Create an image. + RefPtr image = ImageFactory::CreateAnonymousImage( + nsDependentCString(testCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + nsCOMPtr inputStream = LoadFile(testCase.mPath); + ASSERT_TRUE(inputStream); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_NS_SUCCEEDED(rv); + + // Ensures we meet the threshold for FLAG_SYNC_DECODE_IF_FAST to do sync + // decoding without the implications of FLAG_SYNC_DECODE. + ASSERT_LT(length, + static_cast( + StaticPrefs::image_mem_decode_bytes_at_a_time_AtStartup())); + + // Write the data into the image. + rv = image->OnImageDataAvailable(nullptr, inputStream, 0, + static_cast(length)); + ASSERT_NS_SUCCEEDED(rv); + + // Let the image know we've sent all the data. + rv = image->OnImageDataComplete(nullptr, NS_OK, true); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr tracker = image->GetProgressTracker(); + tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE); + + const uint32_t whichFrame = imgIContainer::FRAME_CURRENT; + + // FLAG_SYNC_DECODE will make RasterImage::LookupFrame use + // SurfaceCache::Lookup to force an exact match lookup (and potential decode). + const uint32_t exactFlags = imgIContainer::FLAG_HIGH_QUALITY_SCALING | + imgIContainer::FLAG_SYNC_DECODE; + + // If the data stream is small enough, as we assert above, + // FLAG_SYNC_DECODE_IF_FAST will allow us to decode sync, but avoid forcing + // SurfaceCache::Lookup. Instead it will use SurfaceCache::LookupBestMatch. + const uint32_t bestMatchFlags = imgIContainer::FLAG_HIGH_QUALITY_SCALING | + imgIContainer::FLAG_SYNC_DECODE_IF_FAST; + + // We need the default threshold to be enabled (otherwise we should disable + // this test). + int32_t threshold = StaticPrefs::image_cache_factor2_threshold_surfaces(); + ASSERT_TRUE(threshold >= 0); + + // We need to know what the native sizes are, otherwise factor of 2 mode will + // be disabled. + size_t nativeSizes = image->GetNativeSizesLength(); + ASSERT_EQ(nativeSizes, 1u); + + // Threshold is the native size count and the pref threshold added together. + // Make sure the image is big enough that we can simply decrement and divide + // off the size as we please and not hit unexpected duplicates. + int32_t totalThreshold = static_cast(nativeSizes) + threshold; + ASSERT_TRUE(testCase.mSize.width > totalThreshold * 4); + + // Request a bunch of slightly different sizes. We won't trip factor of 2 mode + // in this loop. + IntSize size = testCase.mSize; + for (int32_t i = 0; i <= totalThreshold; ++i) { + RefPtr surf = + image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize(), size); + + size.width -= 1; + size.height -= 1; + } + + // Now let's ask for a new size. Despite this being sync, it will return + // the closest factor of 2 size we have and not the requested size. + RefPtr surf = + image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + + EXPECT_EQ(surf->GetSize(), testCase.mSize); + + // Now we should be in factor of 2 mode but unless we trigger a decode no + // pruning of the old sized surfaces should happen. + size = testCase.mSize; + for (int32_t i = 0; i < totalThreshold; ++i) { + RefPtr surf = + image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize(), size); + + size.width -= 1; + size.height -= 1; + } + + // Now force an existing surface to be marked as explicit so that it + // won't get freed upon pruning (gets marked in the Lookup). + size.width += 1; + size.height += 1; + surf = image->GetFrameAtSize(size, whichFrame, exactFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize(), size); + + // Now force a new decode to happen by getting a new factor of 2 size. + size.width = testCase.mSize.width / 2 - 1; + size.height = testCase.mSize.height / 2 - 1; + surf = image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize().width, testCase.mSize.width / 2); + EXPECT_EQ(surf->GetSize().height, testCase.mSize.height / 2); + + // The decode above would have forced a pruning to happen, so now if + // we request all of the sizes we used to have decoded, only the explicit + // size should have been kept. + size = testCase.mSize; + for (int32_t i = 0; i < totalThreshold - 1; ++i) { + RefPtr surf = + image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize(), testCase.mSize); + + size.width -= 1; + size.height -= 1; + } + + // This lookup finds the surface that already existed that we later marked + // as explicit. It should still exist after pruning. + surf = image->GetFrameAtSize(size, whichFrame, bestMatchFlags); + ASSERT_TRUE(surf); + EXPECT_EQ(surf->GetSize(), size); +} diff --git a/image/test/gtest/TestSurfacePipeIntegration.cpp b/image/test/gtest/TestSurfacePipeIntegration.cpp new file mode 100644 index 0000000000..0843ce3f77 --- /dev/null +++ b/image/test/gtest/TestSurfacePipeIntegration.cpp @@ -0,0 +1,349 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { +namespace image { + +class TestSurfacePipeFactory { + public: + static SurfacePipe SimpleSurfacePipe() { + SurfacePipe pipe; + return pipe; + } + + template + static SurfacePipe SurfacePipeFromPipeline(T&& aPipeline) { + return SurfacePipe{std::move(aPipeline)}; + } + + private: + TestSurfacePipeFactory() {} +}; + +} // namespace image +} // namespace mozilla + +void CheckSurfacePipeMethodResults(SurfacePipe* aPipe, image::Decoder* aDecoder, + const IntRect& aRect = IntRect(0, 0, 100, + 100)) { + // Check that the pipeline ended up in the state we expect. Note that we're + // explicitly testing the SurfacePipe versions of these methods, so we don't + // want to use AssertCorrectPipelineFinalState() here. + EXPECT_TRUE(aPipe->IsSurfaceFinished()); + Maybe invalidRect = aPipe->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect); + + // Check the generated image. + CheckGeneratedImage(aDecoder, aRect); + + // Reset and clear the image before the next test. + aPipe->ResetToFirstRow(); + EXPECT_FALSE(aPipe->IsSurfaceFinished()); + invalidRect = aPipe->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + uint32_t count = 0; + auto result = aPipe->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Transparent().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + EXPECT_TRUE(aPipe->IsSurfaceFinished()); + invalidRect = aPipe->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect); + + aPipe->ResetToFirstRow(); + EXPECT_FALSE(aPipe->IsSurfaceFinished()); + invalidRect = aPipe->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); +} + +class ImageSurfacePipeIntegration : public ::testing::Test { + protected: + AutoInitializeImageLib mInit; +}; + +TEST_F(ImageSurfacePipeIntegration, SurfacePipe) { + // Test that SurfacePipe objects can be initialized and move constructed. + SurfacePipe pipe = TestSurfacePipeFactory::SimpleSurfacePipe(); + + // Test that SurfacePipe objects can be move assigned. + pipe = TestSurfacePipeFactory::SimpleSurfacePipe(); + + // Test that SurfacePipe objects can be initialized with a pipeline. + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + auto sink = MakeUnique(); + nsresult rv = sink->Configure( + SurfaceConfig{decoder, IntSize(100, 100), SurfaceFormat::OS_RGBA, false}); + ASSERT_NS_SUCCEEDED(rv); + + pipe = TestSurfacePipeFactory::SurfacePipeFromPipeline(sink); + + // Test that WritePixels() gets passed through to the underlying pipeline. + { + uint32_t count = 0; + auto result = pipe.WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + CheckSurfacePipeMethodResults(&pipe, decoder); + } + + // Create a buffer the same size as one row of the surface, containing all + // green pixels. We'll use this for the WriteBuffer() tests. + uint32_t buffer[100]; + for (int i = 0; i < 100; ++i) { + buffer[i] = BGRAColor::Green().AsPixel(); + } + + // Test that WriteBuffer() gets passed through to the underlying pipeline. + { + uint32_t count = 0; + WriteState result = WriteState::NEED_MORE_DATA; + while (result == WriteState::NEED_MORE_DATA) { + result = pipe.WriteBuffer(buffer); + ++count; + } + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + CheckSurfacePipeMethodResults(&pipe, decoder); + } + + // Test that the 3 argument version of WriteBuffer() gets passed through to + // the underlying pipeline. + { + uint32_t count = 0; + WriteState result = WriteState::NEED_MORE_DATA; + while (result == WriteState::NEED_MORE_DATA) { + result = pipe.WriteBuffer(buffer, 0, 100); + ++count; + } + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + CheckSurfacePipeMethodResults(&pipe, decoder); + } + + // Test that WritePixelBlocks() gets passed through to the underlying + // pipeline. + { + uint32_t count = 0; + WriteState result = pipe.WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + ++count; + EXPECT_EQ(int32_t(100), aLength); + memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t)); + return std::make_tuple(int32_t(100), Maybe()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + CheckSurfacePipeMethodResults(&pipe, decoder); + } + + // Test that WriteEmptyRow() gets passed through to the underlying pipeline. + { + uint32_t count = 0; + WriteState result = WriteState::NEED_MORE_DATA; + while (result == WriteState::NEED_MORE_DATA) { + result = pipe.WriteEmptyRow(); + ++count; + } + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + CheckSurfacePipeMethodResults(&pipe, decoder, IntRect(0, 0, 0, 0)); + } + + // Mark the frame as finished so we don't get an assertion. + RawAccessFrameRef currentFrame = decoder->GetCurrentFrameRef(); + currentFrame->Finish(); +} + +TEST_F(ImageSurfacePipeIntegration, DeinterlaceDownscaleWritePixels) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + auto test = [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 25, 25))); + }; + + WithFilterPipeline( + decoder, test, + DeinterlacingConfig{/* mProgressiveDisplay = */ true}, + DownscalingConfig{IntSize(100, 100), SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, IntSize(25, 25), SurfaceFormat::OS_RGBA, false}); +} + +TEST_F(ImageSurfacePipeIntegration, + RemoveFrameRectBottomRightDownscaleWritePixels) { + // This test case uses a frame rect that extends beyond the borders of the + // image to the bottom and to the right. It looks roughly like this (with the + // box made of '#'s representing the frame rect): + // + // +------------+ + // + + + // + +------------+ + // + +############+ + // +------+############+ + // +############+ + // +------------+ + + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + // Note that aInputWriteRect is 100x50 because RemoveFrameRectFilter ignores + // trailing rows that don't show up in the output. (Leading rows unfortunately + // can't be ignored.) So the action of the pipeline is as follows: + // + // (1) RemoveFrameRectFilter reads a 100x50 region of the input. + // (aInputWriteRect captures this fact.) The remaining 50 rows are ignored + // because they extend off the bottom of the image due to the frame rect's + // (50, 50) offset. The 50 columns on the right also don't end up in the + // output, so ultimately only a 50x50 region in the output contains data + // from the input. The filter's output is not 50x50, though, but 100x100, + // because what RemoveFrameRectFilter does is introduce blank rows or + // columns as necessary to transform an image that needs a frame rect into + // an image that doesn't. + // + // (2) DownscalingFilter reads the output of RemoveFrameRectFilter (100x100) + // and downscales it to 20x20. + // + // (3) The surface owned by SurfaceSink logically has only a 10x10 region + // region in it that's non-blank; this is the downscaled version of the + // 50x50 region discussed in (1). (aOutputWriteRect captures this fact.) + // Some fuzz, as usual, is necessary when dealing with Lanczos + // downscaling. + + auto test = [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 50)), + /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)), + /* aFuzz = */ 0x33); + }; + + WithFilterPipeline( + decoder, test, RemoveFrameRectConfig{IntRect(50, 50, 100, 100)}, + DownscalingConfig{IntSize(100, 100), SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, false}); +} + +TEST_F(ImageSurfacePipeIntegration, + RemoveFrameRectTopLeftDownscaleWritePixels) { + // This test case uses a frame rect that extends beyond the borders of the + // image to the top and to the left. It looks roughly like this (with the + // box made of '#'s representing the frame rect): + // + // +------------+ + // +############+ + // +############+------+ + // +############+ + + // +------------+ + + // + + + // +------------+ + + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + auto test = [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)), + /* aOutputWriteRect = */ Some(IntRect(0, 0, 10, 10)), + /* aFuzz = */ 0x21); + }; + + WithFilterPipeline( + decoder, test, RemoveFrameRectConfig{IntRect(-50, -50, 100, 100)}, + DownscalingConfig{IntSize(100, 100), SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, false}); +} + +TEST_F(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectWritePixels) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + // Note that aInputRect is the full 100x100 size even though + // RemoveFrameRectFilter is part of this pipeline, because deinterlacing + // requires reading every row. + + auto test = [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)), + /* aOutputWriteRect = */ Some(IntRect(50, 50, 50, 50))); + }; + + WithFilterPipeline( + decoder, test, + DeinterlacingConfig{/* mProgressiveDisplay = */ true}, + RemoveFrameRectConfig{IntRect(50, 50, 100, 100)}, + SurfaceConfig{decoder, IntSize(100, 100), SurfaceFormat::OS_RGBA, false}); +} + +TEST_F(ImageSurfacePipeIntegration, + DeinterlaceRemoveFrameRectDownscaleWritePixels) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + auto test = [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, + /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)), + /* aInputRect = */ Some(IntRect(0, 0, 100, 100)), + /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)), + /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)), + /* aFuzz = */ 33); + }; + + WithFilterPipeline( + decoder, test, + DeinterlacingConfig{/* mProgressiveDisplay = */ true}, + RemoveFrameRectConfig{IntRect(50, 50, 100, 100)}, + DownscalingConfig{IntSize(100, 100), SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, false}); +} + +TEST_F(ImageSurfacePipeIntegration, ConfiguringHugeDeinterlacingBufferFails) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + // When DownscalingFilter is used, we may succeed in allocating an output + // surface for huge images, because we only need to store the scaled-down + // version of the image. However, regardless of downscaling, + // DeinterlacingFilter needs to allocate a buffer as large as the size of the + // input. This can cause OOMs on operating systems that allow overcommit. This + // test makes sure that we reject such allocations. + AssertConfiguringPipelineFails( + decoder, DeinterlacingConfig{/* mProgressiveDisplay = */ true}, + DownscalingConfig{IntSize(60000, 60000), SurfaceFormat::OS_RGBA}, + SurfaceConfig{decoder, IntSize(600, 600), SurfaceFormat::OS_RGBA, false}); +} diff --git a/image/test/gtest/TestSurfaceSink.cpp b/image/test/gtest/TestSurfaceSink.cpp new file mode 100644 index 0000000000..b205f00e7f --- /dev/null +++ b/image/test/gtest/TestSurfaceSink.cpp @@ -0,0 +1,982 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +enum class Orient { NORMAL, FLIP_VERTICALLY }; + +static void InitializeRowBuffer(uint32_t* aBuffer, size_t aSize, + size_t aStartPixel, size_t aEndPixel, + uint32_t aSetPixel) { + uint32_t transparentPixel = BGRAColor::Transparent().AsPixel(); + for (size_t i = 0; i < aStartPixel && i < aSize; ++i) { + aBuffer[i] = transparentPixel; + } + for (size_t i = aStartPixel; i < aEndPixel && i < aSize; ++i) { + aBuffer[i] = aSetPixel; + } + for (size_t i = aEndPixel; i < aSize; ++i) { + aBuffer[i] = transparentPixel; + } +} + +template +void WithSurfaceSink(Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + const bool flipVertically = Orientation == Orient::FLIP_VERTICALLY; + + WithFilterPipeline(decoder, std::forward(aFunc), + SurfaceConfig{decoder, IntSize(100, 100), + SurfaceFormat::OS_RGBA, flipVertically}); +} + +void ResetForNextPass(SurfaceFilter* aSink) { + aSink->ResetToFirstRow(); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); +} + +template +void DoCheckIterativeWrite(SurfaceFilter* aSink, WriteFunc aWriteFunc, + CheckFunc aCheckFunc) { + // Write the buffer to successive rows until every row of the surface + // has been written. + uint32_t row = 0; + WriteState result = WriteState::NEED_MORE_DATA; + while (result == WriteState::NEED_MORE_DATA) { + result = aWriteFunc(row); + ++row; + } + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, row); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Check that the generated image is correct. + aCheckFunc(); +} + +template +void CheckIterativeWrite(image::Decoder* aDecoder, SurfaceSink* aSink, + const IntRect& aOutputRect, WriteFunc aWriteFunc) { + // Ignore the row passed to WriteFunc, since no callers use it. + auto writeFunc = [&](uint32_t) { return aWriteFunc(); }; + + DoCheckIterativeWrite(aSink, writeFunc, + [&] { CheckGeneratedImage(aDecoder, aOutputRect); }); +} + +TEST(ImageSurfaceSink, SurfaceSinkInitialization) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Check initial state. + EXPECT_FALSE(aSink->IsSurfaceFinished()); + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Check that the surface is zero-initialized. We verify this by calling + // CheckGeneratedImage() and telling it that we didn't write to the + // surface anyway (i.e., we wrote to the empty rect); it will then + // expect the entire surface to be transparent, which is what it should + // be if it was zero-initialied. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 0, 0)); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixels) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + CheckWritePixels(aDecoder, aSink); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelsFinish) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Write nothing into the surface; just finish immediately. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + count++; + return AsVariant(WriteState::FINISHED); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(1u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixels([&]() { + count++; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Transparent())); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelsEarlyExit) +{ + auto checkEarlyExit = [](image::Decoder* aDecoder, SurfaceSink* aSink, + WriteState aState) { + // Write half a row of green pixels and then exit early with |aState|. If + // the lambda keeps getting called, we'll write red pixels, which will cause + // the test to fail. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 50) { + return AsVariant(aState); + } + return count++ < 50 ? AsVariant(BGRAColor::Green().AsPixel()) + : AsVariant(BGRAColor::Red().AsPixel()); + }); + + EXPECT_EQ(aState, result); + EXPECT_EQ(50u, count); + CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1)); + + if (aState != WriteState::FINISHED) { + // We should still be able to write more at this point. + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Verify that we can resume writing. We'll finish up the same row. + count = 0; + result = aSink->WritePixels([&]() -> NextPixel { + if (count == 50) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(50u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 1)); + + return; + } + + // We should've finished the surface at this point. + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixels([&] { + count++; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is still correct. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1)); + }; + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA); + }); + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::FAILURE); + }); + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::FINISHED); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelsToRow) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + // Write the first 99 rows of our 100x100 surface and verify that even + // though our lambda will yield pixels forever, only one row is written + // per call to WritePixelsToRow(). + for (int row = 0; row < 99; ++row) { + uint32_t count = 0; + WriteState result = aSink->WritePixelsToRow([&] { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(100u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, row, 100, 1), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, row, 100, 1), invalidRect->mOutputSpaceRect); + + CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, row + 1)); + } + + // Write the final line, which should finish the surface. + uint32_t count = 0; + WriteState result = aSink->WritePixelsToRow([&] { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + + // Note that the final invalid rect we expect here is only the last row; + // that's because we called TakeInvalidRect() repeatedly in the loop + // above. + AssertCorrectPipelineFinalState(aSink, IntRect(0, 99, 100, 1), + IntRect(0, 99, 100, 1)); + + // Check that the generated image is correct. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixelsToRow([&] { + count++; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is still correct. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 100)); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelsToRowEarlyExit) +{ + auto checkEarlyExit = [](image::Decoder* aDecoder, SurfaceSink* aSink, + WriteState aState) { + // Write half a row of green pixels and then exit early with |aState|. If + // the lambda keeps getting called, we'll write red pixels, which will cause + // the test to fail. + uint32_t count = 0; + auto result = + aSink->WritePixelsToRow([&]() -> NextPixel { + if (count == 50) { + return AsVariant(aState); + } + return count++ < 50 ? AsVariant(BGRAColor::Green().AsPixel()) + : AsVariant(BGRAColor::Red().AsPixel()); + }); + + EXPECT_EQ(aState, result); + EXPECT_EQ(50u, count); + CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1)); + + if (aState != WriteState::FINISHED) { + // We should still be able to write more at this point. + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Verify that we can resume the same row and still stop at the end. + count = 0; + WriteState result = aSink->WritePixelsToRow([&] { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(50u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 1)); + + return; + } + + // We should've finished the surface at this point. + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixelsToRow([&] { + count++; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is still correct. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1)); + }; + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA); + }); + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::FAILURE); + }); + + WithSurfaceSink( + [&](image::Decoder* aDecoder, SurfaceSink* aSink) { + checkEarlyExit(aDecoder, aSink, WriteState::FINISHED); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteBuffer) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Create a green buffer the same size as one row of the surface (which + // is 100x100), containing 60 pixels of green in the middle and 20 + // transparent pixels on either side. + uint32_t buffer[100]; + InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().AsPixel()); + + // Write the buffer to every row of the surface and check that the + // generated image is correct. + CheckIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), + [&] { return aSink->WriteBuffer(buffer); }); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRow) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Create a buffer the same size as one row of the surface, containing + // all green pixels. + uint32_t buffer[100]; + for (int i = 0; i < 100; ++i) { + buffer[i] = BGRAColor::Green().AsPixel(); + } + + // Write the buffer to the middle 60 pixels of every row of the surface + // and check that the generated image is correct. + CheckIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), + [&] { return aSink->WriteBuffer(buffer, 20, 60); }); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRowStartColOverflow) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + // Create a buffer the same size as one row of the surface, containing all + // green pixels. + uint32_t buffer[100]; + for (int i = 0; i < 100; ++i) { + buffer[i] = BGRAColor::Green().AsPixel(); + } + + { + // Write the buffer to successive rows until every row of the surface + // has been written. We place the start column beyond the end of the row, + // which will prevent us from writing anything, so we check that the + // generated image is entirely transparent. + CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), + [&] { return aSink->WriteBuffer(buffer, 100, 100); }); + } + + ResetForNextPass(aSink); + + { + // Write the buffer to successive rows until every row of the surface + // has been written. We use column 50 as the start column, but we still + // write the buffer, which means we overflow the right edge of the surface + // by 50 pixels. We check that the left half of the generated image is + // transparent and the right half is green. + CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), + [&] { return aSink->WriteBuffer(buffer, 50, 100); }); + } + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRowBufferOverflow) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + // Create a buffer twice as large as a row of the surface. The first half + // (which is as large as a row of the image) will contain green pixels, + // while the second half will contain red pixels. + uint32_t buffer[200]; + for (int i = 0; i < 200; ++i) { + buffer[i] = + i < 100 ? BGRAColor::Green().AsPixel() : BGRAColor::Red().AsPixel(); + } + + { + // Write the buffer to successive rows until every row of the surface has + // been written. The buffer extends 100 pixels to the right of a row of + // the surface, but bounds checking will prevent us from overflowing the + // buffer. We check that the generated image is entirely green since the + // pixels on the right side of the buffer shouldn't have been written to + // the surface. + CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 100, 100), + [&] { return aSink->WriteBuffer(buffer, 0, 200); }); + } + + ResetForNextPass(aSink); + + { + // Write from the buffer to the middle of each row of the surface. That + // means that the left side of each row should be transparent, since we + // didn't write anything there. A buffer overflow would cause us to write + // buffer contents into the left side of each row. We check that the + // generated image is transparent on the left side and green on the right. + CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), + [&] { return aSink->WriteBuffer(buffer, 50, 200); }); + } + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteBufferFromNullSource) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Calling WriteBuffer() with a null pointer should fail without making + // any changes to the surface. + uint32_t* nullBuffer = nullptr; + WriteState result = aSink->WriteBuffer(nullBuffer); + + EXPECT_EQ(WriteState::FAILURE, result); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + + // Check that nothing got written to the surface. + CheckGeneratedImage(aDecoder, IntRect(0, 0, 0, 0)); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteEmptyRow) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + { + // Write an empty row to each row of the surface. We check that the + // generated image is entirely transparent. + CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), + [&] { return aSink->WriteEmptyRow(); }); + } + + ResetForNextPass(aSink); + + { + // Write a partial row before we begin calling WriteEmptyRow(). We check + // that the generated image is entirely transparent, which is to be + // expected since WriteEmptyRow() overwrites the current row even if some + // data has already been written to it. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 50) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(50u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), + [&] { return aSink->WriteEmptyRow(); }); + } + + ResetForNextPass(aSink); + + { + // Create a buffer the same size as one row of the surface, containing all + // green pixels. + uint32_t buffer[100]; + for (int i = 0; i < 100; ++i) { + buffer[i] = BGRAColor::Green().AsPixel(); + } + + // Write an empty row to the middle 60 rows of the surface. The first 20 + // and last 20 rows will be green. (We need to use DoCheckIterativeWrite() + // here because we need a custom function to check the output, since it + // can't be described by a simple rect.) + auto writeFunc = [&](uint32_t aRow) { + if (aRow < 20 || aRow >= 80) { + return aSink->WriteBuffer(buffer); + } else { + return aSink->WriteEmptyRow(); + } + }; + + auto checkFunc = [&] { + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Green())); + EXPECT_TRUE( + RowsAreSolidColor(surface, 20, 60, BGRAColor::Transparent())); + EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Green())); + }; + + DoCheckIterativeWrite(aSink, writeFunc, checkFunc); + } + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWriteUnsafeComputedRow) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Create a green buffer the same size as one row of the surface. + uint32_t buffer[100]; + for (int i = 0; i < 100; ++i) { + buffer[i] = BGRAColor::Green().AsPixel(); + } + + // Write the buffer to successive rows until every row of the surface + // has been written. We only write to the right half of each row, so we + // check that the left side of the generated image is transparent and + // the right side is green. + CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&] { + return aSink->WriteUnsafeComputedRow( + [&](uint32_t* aRow, uint32_t aLength) { + EXPECT_EQ(100u, aLength); + memcpy(aRow + 50, buffer, 50 * sizeof(uint32_t)); + }); + }); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelBlocks) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + // Create a green buffer the same size as one row of the surface (which + // is 100x100), containing 60 pixels of green in the middle and 20 + // transparent pixels on either side. + uint32_t buffer[100]; + InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().AsPixel()); + + uint32_t count = 0; + WriteState result = aSink->WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + ++count; + EXPECT_EQ(int32_t(100), aLength); + memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t)); + return std::make_tuple(int32_t(100), Maybe()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Check that the generated image is correct. + CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + count++; + for (int32_t i = 0; i < aLength; ++i) { + aBlockStart[i] = BGRAColor::Red().AsPixel(); + } + return std::make_tuple(aLength, Maybe()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is still correct. + CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100)); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkWritePixelBlocksPartialRow) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + // Create a green buffer the same size as one row of the surface (which is + // 100x100), containing 60 pixels of green in the middle and 20 transparent + // pixels on either side. + uint32_t buffer[100]; + InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().AsPixel()); + + // Write the first 99 rows of our 100x100 surface and verify that even + // though our lambda will yield pixels forever, only one row is written per + // call to WritePixelsToRow(). + for (int row = 0; row < 99; ++row) { + for (int32_t written = 0; written < 100;) { + WriteState result = aSink->WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + // When we write the final block of pixels, it will request we + // start another row. We should abort at that point. + if (aLength == int32_t(100) && written == int32_t(100)) { + return std::make_tuple(int32_t(0), + Some(WriteState::NEED_MORE_DATA)); + } + + // It should always request enough data to fill the row. So it + // should request 100, 75, 50, and finally 25 pixels. + EXPECT_EQ(int32_t(100) - written, aLength); + + // Only write one quarter of the pixels for the row. + memcpy(aBlockStart, &buffer[written], 25 * sizeof(uint32_t)); + written += 25; + + // We've written the last pixels remaining for the row. + if (written == int32_t(100)) { + return std::make_tuple(int32_t(25), Maybe()); + } + + // We've written another quarter of the row but not yet all of it. + return std::make_tuple(int32_t(25), + Some(WriteState::NEED_MORE_DATA)); + }); + + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + } + + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, row, 100, 1), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, row, 100, 1), invalidRect->mOutputSpaceRect); + + CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, row + 1)); + } + + // Write the final line, which should finish the surface. + uint32_t count = 0; + WriteState result = aSink->WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + ++count; + EXPECT_EQ(int32_t(100), aLength); + memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t)); + return std::make_tuple(int32_t(100), Maybe()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(1u, count); + + // Note that the final invalid rect we expect here is only the last row; + // that's because we called TakeInvalidRect() repeatedly in the loop above. + AssertCorrectPipelineFinalState(aSink, IntRect(0, 99, 100, 1), + IntRect(0, 99, 100, 1)); + + // Check that the generated image is correct. + CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100)); + + // Attempt to write more and make sure that nothing gets written. + count = 0; + result = aSink->WritePixelBlocks( + [&](uint32_t* aBlockStart, int32_t aLength) { + count++; + for (int32_t i = 0; i < aLength; ++i) { + aBlockStart[i] = BGRAColor::Red().AsPixel(); + } + return std::make_tuple(aLength, Maybe()); + }); + + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(0u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Check that the generated image is still correct. + CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100)); + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkProgressivePasses) +{ + WithSurfaceSink( + [](image::Decoder* aDecoder, SurfaceSink* aSink) { + { + // Fill the image with a first pass of red. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red())); + } + + { + ResetForNextPass(aSink); + + // Check that the generated image is still the first pass image. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red())); + } + + { + // Fill the image with a second pass of green. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green())); + } + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkInvalidRect) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + { + // Write one row. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 100) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(100u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we have the right invalid rect. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 1), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 0, 100, 1), invalidRect->mOutputSpaceRect); + } + + { + // Write eight rows. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 100 * 8) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(100u * 8u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we have the right invalid rect. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 1, 100, 8), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 1, 100, 8), invalidRect->mOutputSpaceRect); + } + + { + // Write the left half of one row. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 50) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(50u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we don't have an invalid rect, since the invalid rect only + // gets updated when a row gets completed. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + } + + { + // Write the right half of the same row. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 50) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(50u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we have the right invalid rect, which will include both the + // left and right halves of this row now that we've completed it. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 9, 100, 1), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 9, 100, 1), invalidRect->mOutputSpaceRect); + } + + { + // Write no rows. + auto result = aSink->WritePixels( + [&]() { return AsVariant(WriteState::NEED_MORE_DATA); }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we don't have an invalid rect. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isNothing()); + } + + { + // Fill the rest of the image. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 90u, count); + EXPECT_TRUE(aSink->IsSurfaceFinished()); + + // Assert that we have the right invalid rect. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 10, 100, 90), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 10, 100, 90), invalidRect->mOutputSpaceRect); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green())); + } + }); +} + +TEST(ImageSurfaceSink, SurfaceSinkFlipVertically) +{ + WithSurfaceSink([](image::Decoder* aDecoder, + SurfaceSink* aSink) { + { + // Fill the image with a first pass of red. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(100u * 100u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100), + IntRect(0, 0, 100, 100)); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red())); + } + + { + ResetForNextPass(aSink); + + // Check that the generated image is still the first pass image. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red())); + } + + { + // Fill 25 rows of the image with green and make sure everything is OK. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() -> NextPixel { + if (count == 25 * 100) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + count++; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::NEED_MORE_DATA, result); + EXPECT_EQ(25u * 100u, count); + EXPECT_FALSE(aSink->IsSurfaceFinished()); + + // Assert that we have the right invalid rect, which should include the + // *bottom* (since we're flipping vertically) 25 rows of the image. + Maybe invalidRect = aSink->TakeInvalidRect(); + EXPECT_TRUE(invalidRect.isSome()); + EXPECT_EQ(OrientedIntRect(0, 75, 100, 25), invalidRect->mInputSpaceRect); + EXPECT_EQ(OrientedIntRect(0, 75, 100, 25), invalidRect->mOutputSpaceRect); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 75, BGRAColor::Red())); + EXPECT_TRUE(RowsAreSolidColor(surface, 75, 25, BGRAColor::Green())); + } + + { + // Fill the rest of the image with a second pass of green. + uint32_t count = 0; + auto result = aSink->WritePixels([&]() { + ++count; + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + EXPECT_EQ(75u * 100u, count); + + AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 75), + IntRect(0, 0, 100, 75)); + + // Check that the generated image is correct. + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + RefPtr surface = currentFrame->GetSourceSurface(); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green())); + } + }); +} diff --git a/image/test/gtest/TestSwizzleFilter.cpp b/image/test/gtest/TestSwizzleFilter.cpp new file mode 100644 index 0000000000..65faf85155 --- /dev/null +++ b/image/test/gtest/TestSwizzleFilter.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +template +void WithSwizzleFilter(const IntSize& aSize, SurfaceFormat aInputFormat, + SurfaceFormat aOutputFormat, bool aPremultiplyAlpha, + Func aFunc) { + RefPtr decoder = CreateTrivialDecoder(); + ASSERT_TRUE(decoder != nullptr); + + WithFilterPipeline( + decoder, std::forward(aFunc), + SwizzleConfig{aInputFormat, aOutputFormat, aPremultiplyAlpha}, + SurfaceConfig{decoder, aSize, aOutputFormat, false}); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_BGRA) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8A8, + false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, BGRAColor::Blue(), + BGRAColor::Red()); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_BGRA) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8A8, true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels( + aDecoder, aFilter, BGRAColor(0x26, 0x00, 0x00, 0x7F, true), + BGRAColor(0x00, 0x00, 0x26, 0x7F), Nothing(), Nothing(), Nothing(), + Nothing(), /* aFuzz */ 1); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_BGRX) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8X8, + false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, + BGRAColor(0x26, 0x00, 0x00, 0x7F, true), + BGRAColor(0x00, 0x00, 0x26, 0xFF)); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_BGRX) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8X8, true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, + BGRAColor(0x26, 0x00, 0x00, 0x7F, true), + BGRAColor(0x00, 0x00, 0x13, 0xFF)); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_RGBX) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::R8G8B8X8, + false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, + BGRAColor(0x00, 0x00, 0x26, 0x7F, true), + BGRAColor(0x00, 0x00, 0x26, 0xFF)); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_RGRX) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::R8G8B8X8, true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, + BGRAColor(0x00, 0x00, 0x26, 0x7F, true), + BGRAColor(0x00, 0x00, 0x13, 0xFF)); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_BGRA_to_BGRX) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::B8G8R8A8, SurfaceFormat::B8G8R8X8, + false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels(aDecoder, aFilter, + BGRAColor(0x10, 0x26, 0x00, 0x7F, true), + BGRAColor(0x10, 0x26, 0x00, 0xFF)); + }); +} + +TEST(ImageSwizzleFilter, WritePixels_BGRA_to_Premultiplied_BGRA) +{ + WithSwizzleFilter( + IntSize(100, 100), SurfaceFormat::B8G8R8A8, SurfaceFormat::B8G8R8A8, true, + [](image::Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckTransformedWritePixels( + aDecoder, aFilter, BGRAColor(0x10, 0x26, 0x00, 0x7F, true), + BGRAColor(0x10, 0x26, 0x00, 0x7F), Nothing(), Nothing(), Nothing(), + Nothing(), /* aFuzz */ 1); + }); +} diff --git a/image/test/gtest/animated-with-extra-image-sub-blocks.gif b/image/test/gtest/animated-with-extra-image-sub-blocks.gif new file mode 100644 index 0000000000..a145c814a6 Binary files /dev/null and b/image/test/gtest/animated-with-extra-image-sub-blocks.gif differ diff --git a/image/test/gtest/blend.avif b/image/test/gtest/blend.avif new file mode 100644 index 0000000000..f059f04d10 Binary files /dev/null and b/image/test/gtest/blend.avif differ diff --git a/image/test/gtest/blend.gif b/image/test/gtest/blend.gif new file mode 100644 index 0000000000..2f7391454c Binary files /dev/null and b/image/test/gtest/blend.gif differ diff --git a/image/test/gtest/blend.png b/image/test/gtest/blend.png new file mode 100644 index 0000000000..c4e739f068 Binary files /dev/null and b/image/test/gtest/blend.png differ diff --git a/image/test/gtest/blend.webp b/image/test/gtest/blend.webp new file mode 100644 index 0000000000..1b95e6f377 Binary files /dev/null and b/image/test/gtest/blend.webp differ diff --git a/image/test/gtest/bug-1655846.avif b/image/test/gtest/bug-1655846.avif new file mode 100644 index 0000000000..31c7e42454 Binary files /dev/null and b/image/test/gtest/bug-1655846.avif differ diff --git a/image/test/gtest/corrupt-with-bad-bmp-height.ico b/image/test/gtest/corrupt-with-bad-bmp-height.ico new file mode 100644 index 0000000000..ee4a90fcd7 Binary files /dev/null and b/image/test/gtest/corrupt-with-bad-bmp-height.ico differ diff --git a/image/test/gtest/corrupt-with-bad-bmp-width.ico b/image/test/gtest/corrupt-with-bad-bmp-width.ico new file mode 100644 index 0000000000..aa4051cd07 Binary files /dev/null and b/image/test/gtest/corrupt-with-bad-bmp-width.ico differ diff --git a/image/test/gtest/corrupt-with-bad-ico-bpp.ico b/image/test/gtest/corrupt-with-bad-ico-bpp.ico new file mode 100644 index 0000000000..5db4922e34 Binary files /dev/null and b/image/test/gtest/corrupt-with-bad-ico-bpp.ico differ diff --git a/image/test/gtest/corrupt.jpg b/image/test/gtest/corrupt.jpg new file mode 100644 index 0000000000..555a416d7d Binary files /dev/null and b/image/test/gtest/corrupt.jpg differ diff --git a/image/test/gtest/downscaled.avif b/image/test/gtest/downscaled.avif new file mode 100644 index 0000000000..15aa14d240 Binary files /dev/null and b/image/test/gtest/downscaled.avif differ diff --git a/image/test/gtest/downscaled.bmp b/image/test/gtest/downscaled.bmp new file mode 100644 index 0000000000..9e6a29e62b Binary files /dev/null and b/image/test/gtest/downscaled.bmp differ diff --git a/image/test/gtest/downscaled.gif b/image/test/gtest/downscaled.gif new file mode 100644 index 0000000000..ff9a20bcdb Binary files /dev/null and b/image/test/gtest/downscaled.gif differ diff --git a/image/test/gtest/downscaled.ico b/image/test/gtest/downscaled.ico new file mode 100644 index 0000000000..ee112af0a9 Binary files /dev/null and b/image/test/gtest/downscaled.ico differ diff --git a/image/test/gtest/downscaled.icon b/image/test/gtest/downscaled.icon new file mode 100644 index 0000000000..0ec9139866 Binary files /dev/null and b/image/test/gtest/downscaled.icon differ diff --git a/image/test/gtest/downscaled.jpg b/image/test/gtest/downscaled.jpg new file mode 100644 index 0000000000..5a4b3cd036 Binary files /dev/null and b/image/test/gtest/downscaled.jpg differ diff --git a/image/test/gtest/downscaled.jxl b/image/test/gtest/downscaled.jxl new file mode 100644 index 0000000000..90e9f65ab2 Binary files /dev/null and b/image/test/gtest/downscaled.jxl differ diff --git a/image/test/gtest/downscaled.png b/image/test/gtest/downscaled.png new file mode 100644 index 0000000000..b71b4652d5 Binary files /dev/null and b/image/test/gtest/downscaled.png differ diff --git a/image/test/gtest/downscaled.webp b/image/test/gtest/downscaled.webp new file mode 100644 index 0000000000..c2db6d6446 Binary files /dev/null and b/image/test/gtest/downscaled.webp differ diff --git a/image/test/gtest/exif_resolution.jpg b/image/test/gtest/exif_resolution.jpg new file mode 100644 index 0000000000..757ce2d877 Binary files /dev/null and b/image/test/gtest/exif_resolution.jpg differ diff --git a/image/test/gtest/first-frame-green.avif b/image/test/gtest/first-frame-green.avif new file mode 100644 index 0000000000..9f2417664f Binary files /dev/null and b/image/test/gtest/first-frame-green.avif differ diff --git a/image/test/gtest/first-frame-green.gif b/image/test/gtest/first-frame-green.gif new file mode 100644 index 0000000000..cd3c7d3db8 Binary files /dev/null and b/image/test/gtest/first-frame-green.gif differ diff --git a/image/test/gtest/first-frame-green.png b/image/test/gtest/first-frame-green.png new file mode 100644 index 0000000000..115f035d89 Binary files /dev/null and b/image/test/gtest/first-frame-green.png differ diff --git a/image/test/gtest/first-frame-green.webp b/image/test/gtest/first-frame-green.webp new file mode 100644 index 0000000000..44db5c71c3 Binary files /dev/null and b/image/test/gtest/first-frame-green.webp differ diff --git a/image/test/gtest/first-frame-padding.gif b/image/test/gtest/first-frame-padding.gif new file mode 100644 index 0000000000..e6d7c49322 Binary files /dev/null and b/image/test/gtest/first-frame-padding.gif differ diff --git a/image/test/gtest/gray-235-10bit-full-range-bt2020.avif b/image/test/gtest/gray-235-10bit-full-range-bt2020.avif new file mode 100644 index 0000000000..33603adc6e Binary files /dev/null and b/image/test/gtest/gray-235-10bit-full-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-10bit-full-range-bt601.avif b/image/test/gtest/gray-235-10bit-full-range-bt601.avif new file mode 100644 index 0000000000..d8bbd550b1 Binary files /dev/null and b/image/test/gtest/gray-235-10bit-full-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-10bit-full-range-bt709.avif b/image/test/gtest/gray-235-10bit-full-range-bt709.avif new file mode 100644 index 0000000000..a2bae8ce21 Binary files /dev/null and b/image/test/gtest/gray-235-10bit-full-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-10bit-full-range-grayscale.avif b/image/test/gtest/gray-235-10bit-full-range-grayscale.avif new file mode 100644 index 0000000000..bb64ee7bfe Binary files /dev/null and b/image/test/gtest/gray-235-10bit-full-range-grayscale.avif differ diff --git a/image/test/gtest/gray-235-10bit-limited-range-bt2020.avif b/image/test/gtest/gray-235-10bit-limited-range-bt2020.avif new file mode 100644 index 0000000000..13ab0d258e Binary files /dev/null and b/image/test/gtest/gray-235-10bit-limited-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-10bit-limited-range-bt601.avif b/image/test/gtest/gray-235-10bit-limited-range-bt601.avif new file mode 100644 index 0000000000..e0bc194c3f Binary files /dev/null and b/image/test/gtest/gray-235-10bit-limited-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-10bit-limited-range-bt709.avif b/image/test/gtest/gray-235-10bit-limited-range-bt709.avif new file mode 100644 index 0000000000..bff8d79aa8 Binary files /dev/null and b/image/test/gtest/gray-235-10bit-limited-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-10bit-limited-range-grayscale.avif b/image/test/gtest/gray-235-10bit-limited-range-grayscale.avif new file mode 100644 index 0000000000..297be0f2bc Binary files /dev/null and b/image/test/gtest/gray-235-10bit-limited-range-grayscale.avif differ diff --git a/image/test/gtest/gray-235-12bit-full-range-bt2020.avif b/image/test/gtest/gray-235-12bit-full-range-bt2020.avif new file mode 100644 index 0000000000..5f42432364 Binary files /dev/null and b/image/test/gtest/gray-235-12bit-full-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-12bit-full-range-bt601.avif b/image/test/gtest/gray-235-12bit-full-range-bt601.avif new file mode 100644 index 0000000000..e681f44356 Binary files /dev/null and b/image/test/gtest/gray-235-12bit-full-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-12bit-full-range-bt709.avif b/image/test/gtest/gray-235-12bit-full-range-bt709.avif new file mode 100644 index 0000000000..608e41d54b Binary files /dev/null and b/image/test/gtest/gray-235-12bit-full-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-12bit-full-range-grayscale.avif b/image/test/gtest/gray-235-12bit-full-range-grayscale.avif new file mode 100644 index 0000000000..9ced94abfd Binary files /dev/null and b/image/test/gtest/gray-235-12bit-full-range-grayscale.avif differ diff --git a/image/test/gtest/gray-235-12bit-limited-range-bt2020.avif b/image/test/gtest/gray-235-12bit-limited-range-bt2020.avif new file mode 100644 index 0000000000..1673a67d21 Binary files /dev/null and b/image/test/gtest/gray-235-12bit-limited-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-12bit-limited-range-bt601.avif b/image/test/gtest/gray-235-12bit-limited-range-bt601.avif new file mode 100644 index 0000000000..fbd1938c90 Binary files /dev/null and b/image/test/gtest/gray-235-12bit-limited-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-12bit-limited-range-bt709.avif b/image/test/gtest/gray-235-12bit-limited-range-bt709.avif new file mode 100644 index 0000000000..38e4bbff7c Binary files /dev/null and b/image/test/gtest/gray-235-12bit-limited-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-12bit-limited-range-grayscale.avif b/image/test/gtest/gray-235-12bit-limited-range-grayscale.avif new file mode 100644 index 0000000000..415b09afe5 Binary files /dev/null and b/image/test/gtest/gray-235-12bit-limited-range-grayscale.avif differ diff --git a/image/test/gtest/gray-235-8bit-full-range-bt2020.avif b/image/test/gtest/gray-235-8bit-full-range-bt2020.avif new file mode 100644 index 0000000000..ad5aea1fe2 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-full-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-8bit-full-range-bt601.avif b/image/test/gtest/gray-235-8bit-full-range-bt601.avif new file mode 100644 index 0000000000..a4692b2110 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-full-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-8bit-full-range-bt709.avif b/image/test/gtest/gray-235-8bit-full-range-bt709.avif new file mode 100644 index 0000000000..80c3406dd2 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-full-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-8bit-full-range-grayscale.avif b/image/test/gtest/gray-235-8bit-full-range-grayscale.avif new file mode 100644 index 0000000000..39abf34204 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-full-range-grayscale.avif differ diff --git a/image/test/gtest/gray-235-8bit-limited-range-bt2020.avif b/image/test/gtest/gray-235-8bit-limited-range-bt2020.avif new file mode 100644 index 0000000000..30782d98d6 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-limited-range-bt2020.avif differ diff --git a/image/test/gtest/gray-235-8bit-limited-range-bt601.avif b/image/test/gtest/gray-235-8bit-limited-range-bt601.avif new file mode 100644 index 0000000000..ccb10e3e57 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-limited-range-bt601.avif differ diff --git a/image/test/gtest/gray-235-8bit-limited-range-bt709.avif b/image/test/gtest/gray-235-8bit-limited-range-bt709.avif new file mode 100644 index 0000000000..350fed532d Binary files /dev/null and b/image/test/gtest/gray-235-8bit-limited-range-bt709.avif differ diff --git a/image/test/gtest/gray-235-8bit-limited-range-grayscale.avif b/image/test/gtest/gray-235-8bit-limited-range-grayscale.avif new file mode 100644 index 0000000000..f4dc38f160 Binary files /dev/null and b/image/test/gtest/gray-235-8bit-limited-range-grayscale.avif differ diff --git a/image/test/gtest/green-1x1-truncated.gif b/image/test/gtest/green-1x1-truncated.gif new file mode 100644 index 0000000000..0829f9694d Binary files /dev/null and b/image/test/gtest/green-1x1-truncated.gif differ diff --git a/image/test/gtest/green-large-bmp.ico b/image/test/gtest/green-large-bmp.ico new file mode 100644 index 0000000000..3962cea29d Binary files /dev/null and b/image/test/gtest/green-large-bmp.ico differ diff --git a/image/test/gtest/green-large-png.ico b/image/test/gtest/green-large-png.ico new file mode 100644 index 0000000000..27b9f43cdd Binary files /dev/null and b/image/test/gtest/green-large-png.ico differ diff --git a/image/test/gtest/green-multiple-sizes.ico b/image/test/gtest/green-multiple-sizes.ico new file mode 100644 index 0000000000..b9463d0c89 Binary files /dev/null and b/image/test/gtest/green-multiple-sizes.ico differ diff --git a/image/test/gtest/green.avif b/image/test/gtest/green.avif new file mode 100644 index 0000000000..3178de7f5e Binary files /dev/null and b/image/test/gtest/green.avif differ diff --git a/image/test/gtest/green.bmp b/image/test/gtest/green.bmp new file mode 100644 index 0000000000..f79dd672ad Binary files /dev/null and b/image/test/gtest/green.bmp differ diff --git a/image/test/gtest/green.gif b/image/test/gtest/green.gif new file mode 100644 index 0000000000..ef215dfc94 Binary files /dev/null and b/image/test/gtest/green.gif differ diff --git a/image/test/gtest/green.icc_srgb.webp b/image/test/gtest/green.icc_srgb.webp new file mode 100644 index 0000000000..2a869b447b Binary files /dev/null and b/image/test/gtest/green.icc_srgb.webp differ diff --git a/image/test/gtest/green.ico b/image/test/gtest/green.ico new file mode 100644 index 0000000000..c5dfa8b538 Binary files /dev/null and b/image/test/gtest/green.ico differ diff --git a/image/test/gtest/green.icon b/image/test/gtest/green.icon new file mode 100644 index 0000000000..1de4eeb783 Binary files /dev/null and b/image/test/gtest/green.icon differ diff --git a/image/test/gtest/green.jpg b/image/test/gtest/green.jpg new file mode 100644 index 0000000000..48c454d27c Binary files /dev/null and b/image/test/gtest/green.jpg differ diff --git a/image/test/gtest/green.jxl b/image/test/gtest/green.jxl new file mode 100644 index 0000000000..357f8f392e Binary files /dev/null and b/image/test/gtest/green.jxl differ diff --git a/image/test/gtest/green.png b/image/test/gtest/green.png new file mode 100644 index 0000000000..7df25f33bd Binary files /dev/null and b/image/test/gtest/green.png differ diff --git a/image/test/gtest/green.webp b/image/test/gtest/green.webp new file mode 100644 index 0000000000..04b7f003b4 Binary files /dev/null and b/image/test/gtest/green.webp differ diff --git a/image/test/gtest/hdlr-nonzero-reserved-bug-1727033.avif b/image/test/gtest/hdlr-nonzero-reserved-bug-1727033.avif new file mode 100644 index 0000000000..e84ba63e2d Binary files /dev/null and b/image/test/gtest/hdlr-nonzero-reserved-bug-1727033.avif differ diff --git a/image/test/gtest/invalid-truncated-metadata.bmp b/image/test/gtest/invalid-truncated-metadata.bmp new file mode 100644 index 0000000000..228c5c9992 Binary files /dev/null and b/image/test/gtest/invalid-truncated-metadata.bmp differ diff --git a/image/test/gtest/large.avif b/image/test/gtest/large.avif new file mode 100644 index 0000000000..fbdf084148 Binary files /dev/null and b/image/test/gtest/large.avif differ diff --git a/image/test/gtest/large.jxl b/image/test/gtest/large.jxl new file mode 100644 index 0000000000..a244e66b7d Binary files /dev/null and b/image/test/gtest/large.jxl differ diff --git a/image/test/gtest/large.webp b/image/test/gtest/large.webp new file mode 100644 index 0000000000..9bf0b64fa8 Binary files /dev/null and b/image/test/gtest/large.webp differ diff --git a/image/test/gtest/moz.build b/image/test/gtest/moz.build new file mode 100644 index 0000000000..d563ec202c --- /dev/null +++ b/image/test/gtest/moz.build @@ -0,0 +1,156 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Library("imagetest") + +UNIFIED_SOURCES = [ + "Common.cpp", + "TestADAM7InterpolatingFilter.cpp", + "TestAnimationFrameBuffer.cpp", + "TestBlendAnimationFilter.cpp", + "TestCopyOnWrite.cpp", + "TestDeinterlacingFilter.cpp", + "TestFrameAnimator.cpp", + "TestLoader.cpp", + "TestRemoveFrameRectFilter.cpp", + "TestStreamingLexer.cpp", + "TestSurfaceSink.cpp", + "TestSwizzleFilter.cpp", +] + +# skip the test on windows10-aarch64, aarch64 due to 1544961 +if not (CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CPU_ARCH"] == "aarch64"): + UNIFIED_SOURCES += [ + "TestDecoders.cpp", + "TestDecodersPerf.cpp", + "TestDecodeToSurface.cpp", + "TestMetadata.cpp", + "TestSourceBuffer.cpp", + "TestSurfaceCache.cpp", + ] + +UNIFIED_SOURCES += [ + "TestDownscalingFilter.cpp", + "TestSurfacePipeIntegration.cpp", +] + +TEST_HARNESS_FILES.gtest += [ + "animated-with-extra-image-sub-blocks.gif", + "blend.avif", + "blend.gif", + "blend.png", + "blend.webp", + "bug-1655846.avif", + "corrupt-with-bad-bmp-height.ico", + "corrupt-with-bad-bmp-width.ico", + "corrupt-with-bad-ico-bpp.ico", + "corrupt.jpg", + "downscaled.avif", + "downscaled.bmp", + "downscaled.gif", + "downscaled.ico", + "downscaled.icon", + "downscaled.jpg", + "downscaled.jxl", + "downscaled.png", + "downscaled.webp", + "exif_resolution.jpg", + "first-frame-green.avif", + "first-frame-green.gif", + "first-frame-green.png", + "first-frame-green.webp", + "first-frame-padding.gif", + "gray-235-10bit-full-range-bt2020.avif", + "gray-235-10bit-full-range-bt601.avif", + "gray-235-10bit-full-range-bt709.avif", + "gray-235-10bit-full-range-grayscale.avif", + "gray-235-10bit-limited-range-bt2020.avif", + "gray-235-10bit-limited-range-bt601.avif", + "gray-235-10bit-limited-range-bt709.avif", + "gray-235-10bit-limited-range-grayscale.avif", + "gray-235-12bit-full-range-bt2020.avif", + "gray-235-12bit-full-range-bt601.avif", + "gray-235-12bit-full-range-bt709.avif", + "gray-235-12bit-full-range-grayscale.avif", + "gray-235-12bit-limited-range-bt2020.avif", + "gray-235-12bit-limited-range-bt601.avif", + "gray-235-12bit-limited-range-bt709.avif", + "gray-235-12bit-limited-range-grayscale.avif", + "gray-235-8bit-full-range-bt2020.avif", + "gray-235-8bit-full-range-bt601.avif", + "gray-235-8bit-full-range-bt709.avif", + "gray-235-8bit-full-range-grayscale.avif", + "gray-235-8bit-limited-range-bt2020.avif", + "gray-235-8bit-limited-range-bt601.avif", + "gray-235-8bit-limited-range-bt709.avif", + "gray-235-8bit-limited-range-grayscale.avif", + "green-1x1-truncated.gif", + "green-large-bmp.ico", + "green-large-png.ico", + "green-multiple-sizes.ico", + "green.avif", + "green.bmp", + "green.gif", + "green.icc_srgb.webp", + "green.ico", + "green.icon", + "green.jpg", + "green.jxl", + "green.png", + "green.webp", + "hdlr-nonzero-reserved-bug-1727033.avif", + "invalid-truncated-metadata.bmp", + "large.avif", + "large.jxl", + "large.webp", + "multilayer.avif", + "no-frame-delay.gif", + "perf_cmyk.jpg", + "perf_gray.jpg", + "perf_gray.png", + "perf_gray_alpha.png", + "perf_srgb.gif", + "perf_srgb.png", + "perf_srgb_alpha.png", + "perf_srgb_alpha_lossless.webp", + "perf_srgb_alpha_lossy.webp", + "perf_srgb_lossless.webp", + "perf_srgb_lossy.webp", + "perf_ycbcr.jpg", + "rle4.bmp", + "rle8.bmp", + "stackcheck.avif", + "transparent-green-50pct-10bit-yuv420.avif", + "transparent-green-50pct-10bit-yuv422.avif", + "transparent-green-50pct-10bit-yuv444.avif", + "transparent-green-50pct-12bit-yuv420.avif", + "transparent-green-50pct-12bit-yuv422.avif", + "transparent-green-50pct-12bit-yuv444.avif", + "transparent-green-50pct-8bit-yuv420.avif", + "transparent-green-50pct-8bit-yuv422.avif", + "transparent-green-50pct-8bit-yuv444.avif", + "transparent-ico-with-and-mask.ico", + "transparent-if-within-ico.bmp", + "transparent-no-alpha-header.webp", + "transparent.avif", + "transparent.gif", + "transparent.jxl", + "transparent.png", + "transparent.webp", + "valid-avif-colr-nclx-and-prof.avif", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", + "/gfx/2d", + "/image", +] + +LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/image/test/gtest/multilayer.avif b/image/test/gtest/multilayer.avif new file mode 100644 index 0000000000..91857fde54 Binary files /dev/null and b/image/test/gtest/multilayer.avif differ diff --git a/image/test/gtest/no-frame-delay.gif b/image/test/gtest/no-frame-delay.gif new file mode 100644 index 0000000000..1c50b67431 Binary files /dev/null and b/image/test/gtest/no-frame-delay.gif differ diff --git a/image/test/gtest/perf_cmyk.jpg b/image/test/gtest/perf_cmyk.jpg new file mode 100644 index 0000000000..e9d329f21e Binary files /dev/null and b/image/test/gtest/perf_cmyk.jpg differ diff --git a/image/test/gtest/perf_gray.jpg b/image/test/gtest/perf_gray.jpg new file mode 100644 index 0000000000..ed75b91550 Binary files /dev/null and b/image/test/gtest/perf_gray.jpg differ diff --git a/image/test/gtest/perf_gray.png b/image/test/gtest/perf_gray.png new file mode 100644 index 0000000000..df16c72fb6 Binary files /dev/null and b/image/test/gtest/perf_gray.png differ diff --git a/image/test/gtest/perf_gray_alpha.png b/image/test/gtest/perf_gray_alpha.png new file mode 100644 index 0000000000..fc38ec549b Binary files /dev/null and b/image/test/gtest/perf_gray_alpha.png differ diff --git a/image/test/gtest/perf_srgb.gif b/image/test/gtest/perf_srgb.gif new file mode 100644 index 0000000000..4dadf118b5 Binary files /dev/null and b/image/test/gtest/perf_srgb.gif differ diff --git a/image/test/gtest/perf_srgb.png b/image/test/gtest/perf_srgb.png new file mode 100644 index 0000000000..21f28081c2 Binary files /dev/null and b/image/test/gtest/perf_srgb.png differ diff --git a/image/test/gtest/perf_srgb_alpha.png b/image/test/gtest/perf_srgb_alpha.png new file mode 100644 index 0000000000..1fa7fed59b Binary files /dev/null and b/image/test/gtest/perf_srgb_alpha.png differ diff --git a/image/test/gtest/perf_srgb_alpha_lossless.webp b/image/test/gtest/perf_srgb_alpha_lossless.webp new file mode 100644 index 0000000000..cce4c24ff4 Binary files /dev/null and b/image/test/gtest/perf_srgb_alpha_lossless.webp differ diff --git a/image/test/gtest/perf_srgb_alpha_lossy.webp b/image/test/gtest/perf_srgb_alpha_lossy.webp new file mode 100644 index 0000000000..1bc08edc7d Binary files /dev/null and b/image/test/gtest/perf_srgb_alpha_lossy.webp differ diff --git a/image/test/gtest/perf_srgb_lossless.webp b/image/test/gtest/perf_srgb_lossless.webp new file mode 100644 index 0000000000..ae85a41237 Binary files /dev/null and b/image/test/gtest/perf_srgb_lossless.webp differ diff --git a/image/test/gtest/perf_srgb_lossy.webp b/image/test/gtest/perf_srgb_lossy.webp new file mode 100644 index 0000000000..3caad7ceca Binary files /dev/null and b/image/test/gtest/perf_srgb_lossy.webp differ diff --git a/image/test/gtest/perf_ycbcr.jpg b/image/test/gtest/perf_ycbcr.jpg new file mode 100644 index 0000000000..d2ad4e2b20 Binary files /dev/null and b/image/test/gtest/perf_ycbcr.jpg differ diff --git a/image/test/gtest/rle4.bmp b/image/test/gtest/rle4.bmp new file mode 100644 index 0000000000..78a0927870 Binary files /dev/null and b/image/test/gtest/rle4.bmp differ diff --git a/image/test/gtest/rle8.bmp b/image/test/gtest/rle8.bmp new file mode 100644 index 0000000000..bd793b6b66 Binary files /dev/null and b/image/test/gtest/rle8.bmp differ diff --git a/image/test/gtest/stackcheck.avif b/image/test/gtest/stackcheck.avif new file mode 100644 index 0000000000..fbc9c34dee Binary files /dev/null and b/image/test/gtest/stackcheck.avif differ diff --git a/image/test/gtest/transparent-green-50pct-10bit-yuv420.avif b/image/test/gtest/transparent-green-50pct-10bit-yuv420.avif new file mode 100644 index 0000000000..453840f40c Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-10bit-yuv420.avif differ diff --git a/image/test/gtest/transparent-green-50pct-10bit-yuv422.avif b/image/test/gtest/transparent-green-50pct-10bit-yuv422.avif new file mode 100644 index 0000000000..91e0e75417 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-10bit-yuv422.avif differ diff --git a/image/test/gtest/transparent-green-50pct-10bit-yuv444.avif b/image/test/gtest/transparent-green-50pct-10bit-yuv444.avif new file mode 100644 index 0000000000..65f265ed96 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-10bit-yuv444.avif differ diff --git a/image/test/gtest/transparent-green-50pct-12bit-yuv420.avif b/image/test/gtest/transparent-green-50pct-12bit-yuv420.avif new file mode 100644 index 0000000000..9945c50a46 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-12bit-yuv420.avif differ diff --git a/image/test/gtest/transparent-green-50pct-12bit-yuv422.avif b/image/test/gtest/transparent-green-50pct-12bit-yuv422.avif new file mode 100644 index 0000000000..f428c51f08 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-12bit-yuv422.avif differ diff --git a/image/test/gtest/transparent-green-50pct-12bit-yuv444.avif b/image/test/gtest/transparent-green-50pct-12bit-yuv444.avif new file mode 100644 index 0000000000..811acc3a10 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-12bit-yuv444.avif differ diff --git a/image/test/gtest/transparent-green-50pct-8bit-yuv420.avif b/image/test/gtest/transparent-green-50pct-8bit-yuv420.avif new file mode 100644 index 0000000000..25c1940bde Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-8bit-yuv420.avif differ diff --git a/image/test/gtest/transparent-green-50pct-8bit-yuv422.avif b/image/test/gtest/transparent-green-50pct-8bit-yuv422.avif new file mode 100644 index 0000000000..2f58be6441 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-8bit-yuv422.avif differ diff --git a/image/test/gtest/transparent-green-50pct-8bit-yuv444.avif b/image/test/gtest/transparent-green-50pct-8bit-yuv444.avif new file mode 100644 index 0000000000..ab83b3ba77 Binary files /dev/null and b/image/test/gtest/transparent-green-50pct-8bit-yuv444.avif differ diff --git a/image/test/gtest/transparent-ico-with-and-mask.ico b/image/test/gtest/transparent-ico-with-and-mask.ico new file mode 100644 index 0000000000..ab0dc4bce1 Binary files /dev/null and b/image/test/gtest/transparent-ico-with-and-mask.ico differ diff --git a/image/test/gtest/transparent-if-within-ico.bmp b/image/test/gtest/transparent-if-within-ico.bmp new file mode 100644 index 0000000000..4dc04c181b Binary files /dev/null and b/image/test/gtest/transparent-if-within-ico.bmp differ diff --git a/image/test/gtest/transparent-no-alpha-header.webp b/image/test/gtest/transparent-no-alpha-header.webp new file mode 100644 index 0000000000..8ddd73ac7a Binary files /dev/null and b/image/test/gtest/transparent-no-alpha-header.webp differ diff --git a/image/test/gtest/transparent.avif b/image/test/gtest/transparent.avif new file mode 100644 index 0000000000..00ef35bf74 Binary files /dev/null and b/image/test/gtest/transparent.avif differ diff --git a/image/test/gtest/transparent.gif b/image/test/gtest/transparent.gif new file mode 100644 index 0000000000..48f5c7caf1 Binary files /dev/null and b/image/test/gtest/transparent.gif differ diff --git a/image/test/gtest/transparent.jxl b/image/test/gtest/transparent.jxl new file mode 100644 index 0000000000..c479164516 Binary files /dev/null and b/image/test/gtest/transparent.jxl differ diff --git a/image/test/gtest/transparent.png b/image/test/gtest/transparent.png new file mode 100644 index 0000000000..fc8002053a Binary files /dev/null and b/image/test/gtest/transparent.png differ diff --git a/image/test/gtest/transparent.webp b/image/test/gtest/transparent.webp new file mode 100644 index 0000000000..87b9520521 Binary files /dev/null and b/image/test/gtest/transparent.webp differ diff --git a/image/test/gtest/valid-avif-colr-nclx-and-prof.avif b/image/test/gtest/valid-avif-colr-nclx-and-prof.avif new file mode 100644 index 0000000000..683baa7f54 Binary files /dev/null and b/image/test/gtest/valid-avif-colr-nclx-and-prof.avif differ diff --git a/image/test/mochitest/12M-pixels-1.png b/image/test/mochitest/12M-pixels-1.png new file mode 100644 index 0000000000..f802dd5396 Binary files /dev/null and b/image/test/mochitest/12M-pixels-1.png differ diff --git a/image/test/mochitest/12M-pixels-2.png b/image/test/mochitest/12M-pixels-2.png new file mode 100644 index 0000000000..a6d430442e Binary files /dev/null and b/image/test/mochitest/12M-pixels-2.png differ diff --git a/image/test/mochitest/6M-pixels.png b/image/test/mochitest/6M-pixels.png new file mode 100644 index 0000000000..c813d8b569 Binary files /dev/null and b/image/test/mochitest/6M-pixels.png differ diff --git a/image/test/mochitest/INT32_MIN.bmp b/image/test/mochitest/INT32_MIN.bmp new file mode 100644 index 0000000000..d9a0016107 Binary files /dev/null and b/image/test/mochitest/INT32_MIN.bmp differ diff --git a/image/test/mochitest/animated-avif.avif b/image/test/mochitest/animated-avif.avif new file mode 100644 index 0000000000..f3cafef1e3 Binary files /dev/null and b/image/test/mochitest/animated-avif.avif differ diff --git a/image/test/mochitest/animated-gif-finalframe.gif b/image/test/mochitest/animated-gif-finalframe.gif new file mode 100644 index 0000000000..4e80d31a72 Binary files /dev/null and b/image/test/mochitest/animated-gif-finalframe.gif differ diff --git a/image/test/mochitest/animated-gif.gif b/image/test/mochitest/animated-gif.gif new file mode 100644 index 0000000000..001cbfb87a Binary files /dev/null and b/image/test/mochitest/animated-gif.gif differ diff --git a/image/test/mochitest/animated-gif2.gif b/image/test/mochitest/animated-gif2.gif new file mode 100644 index 0000000000..c66cc4b734 Binary files /dev/null and b/image/test/mochitest/animated-gif2.gif differ diff --git a/image/test/mochitest/animated-gif_trailing-garbage.gif b/image/test/mochitest/animated-gif_trailing-garbage.gif new file mode 100644 index 0000000000..02f4de2e31 Binary files /dev/null and b/image/test/mochitest/animated-gif_trailing-garbage.gif differ diff --git a/image/test/mochitest/animated1.gif b/image/test/mochitest/animated1.gif new file mode 100644 index 0000000000..2f9d8a512b Binary files /dev/null and b/image/test/mochitest/animated1.gif differ diff --git a/image/test/mochitest/animated1.svg b/image/test/mochitest/animated1.svg new file mode 100644 index 0000000000..87118c4ea0 --- /dev/null +++ b/image/test/mochitest/animated1.svg @@ -0,0 +1,12 @@ + + + + diff --git a/image/test/mochitest/animated2.gif b/image/test/mochitest/animated2.gif new file mode 100644 index 0000000000..2f9d8a512b Binary files /dev/null and b/image/test/mochitest/animated2.gif differ diff --git a/image/test/mochitest/animatedMask.gif b/image/test/mochitest/animatedMask.gif new file mode 100644 index 0000000000..72a1c51ddc Binary files /dev/null and b/image/test/mochitest/animatedMask.gif differ diff --git a/image/test/mochitest/animation.svg b/image/test/mochitest/animation.svg new file mode 100644 index 0000000000..2141d86791 --- /dev/null +++ b/image/test/mochitest/animation.svg @@ -0,0 +1,5 @@ + + + + diff --git a/image/test/mochitest/animationPolling.js b/image/test/mochitest/animationPolling.js new file mode 100644 index 0000000000..f20377cf9d --- /dev/null +++ b/image/test/mochitest/animationPolling.js @@ -0,0 +1,469 @@ +// This file expects imgutils.js to be loaded as well. +/* import-globals-from imgutils.js */ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +var currentTest; +var gIsRefImageLoaded = false; +const gShouldOutputDebugInfo = false; + +function pollForSuccess() { + if (!currentTest.isTestFinished) { + if ( + !currentTest.reusingReferenceImage || + (currentTest.reusingReferenceImage && gIsRefImageLoaded) + ) { + currentTest.checkImage(); + } + + setTimeout(pollForSuccess, currentTest.pollFreq); + } +} + +function reuseImageCallback() { + gIsRefImageLoaded = true; +} + +function failTest() { + if (currentTest.isTestFinished || currentTest.closeFunc) { + return; + } + + ok( + false, + "timing out after " + + currentTest.timeout + + "ms. " + + "Animated image still doesn't look correct, after poll #" + + currentTest.pollCounter + ); + currentTest.wereFailures = true; + + if (currentTest.currentSnapshotDataURI) { + currentTest.outputDebugInfo( + "Snapshot #" + currentTest.pollCounter, + "snapNum" + currentTest.pollCounter, + currentTest.currentSnapshotDataURI + ); + } + + currentTest.enableDisplay( + document.getElementById(currentTest.debugElementId) + ); + + currentTest.cleanUpAndFinish(); +} + +/** + * Create a new AnimationTest object. + * + * @param pollFreq The amount of time (in ms) to wait between consecutive + * snapshots if the reference image and the test image don't match. + * @param timeout The total amount of time (in ms) to wait before declaring the + * test as failed. + * @param referenceElementId The id attribute of the reference image element, or + * the source of the image to change to, once the reference snapshot has + * been successfully taken. This latter option could be used if you don't + * want the image to become invisible at any time during the test. + * @param imageElementId The id attribute of the test image element. + * @param debugElementId The id attribute of the div where links should be + * appended if the test fails. + * @param cleanId The id attribute of the div or element to use as the 'clean' + * test. This element is only enabled when we are testing to verify that + * the reference image has been loaded. It can be undefined. + * @param srcAttr The location of the source of the image, for preloading. This + * is usually not required, but it useful for preloading reference + * images. + * @param xulTest A boolean value indicating whether or not this is a XUL test + * (uses hidden=true/false rather than display: none to hide/show + * elements). + * @param closeFunc A function that should be called when this test is finished. + * If null, then cleanUpAndFinish() will be called. This can be used to + * chain tests together, so they are all finished exactly once. + * @returns {AnimationTest} + */ +function AnimationTest( + pollFreq, + timeout, + referenceElementId, + imageElementId, + debugElementId, + cleanId, + srcAttr, + xulTest, + closeFunc +) { + // We want to test the cold loading behavior, so clear cache in case an + // earlier test got our image in there already. + clearAllImageCaches(); + + this.wereFailures = false; + this.pollFreq = pollFreq; + this.timeout = timeout; + this.imageElementId = imageElementId; + this.referenceElementId = referenceElementId; + + if (!document.getElementById(referenceElementId)) { + // In this case, we're assuming the user passed in a string that + // indicates the source of the image they want to change to, + // after the reference image has been taken. + this.reusingImageAsReference = true; + } + + this.srcAttr = srcAttr; + this.debugElementId = debugElementId; + this.referenceSnapshot = ""; // value will be set in takeReferenceSnapshot() + this.pollCounter = 0; + this.isTestFinished = false; + this.numRefsTaken = 0; + this.blankWaitTime = 0; + + this.cleanId = cleanId ? cleanId : ""; + this.xulTest = xulTest ? xulTest : ""; + this.closeFunc = closeFunc ? closeFunc : ""; +} + +AnimationTest.prototype.preloadImage = function () { + if (this.srcAttr) { + this.myImage = new Image(); + this.myImage.onload = function () { + currentTest.continueTest(); + }; + this.myImage.src = this.srcAttr; + } else { + this.continueTest(); + } +}; + +AnimationTest.prototype.outputDebugInfo = function (message, id, dataUri) { + if (!gShouldOutputDebugInfo) { + return; + } + var debugElement = document.getElementById(this.debugElementId); + var newDataUriElement = document.createElement("a"); + newDataUriElement.setAttribute("id", id); + newDataUriElement.setAttribute("href", dataUri); + newDataUriElement.appendChild(document.createTextNode(message)); + debugElement.appendChild(newDataUriElement); + var brElement = document.createElement("br"); + debugElement.appendChild(brElement); + todo(false, "Debug (" + id + "): " + message + " " + dataUri); +}; + +AnimationTest.prototype.isFinished = function () { + return this.isTestFinished; +}; + +AnimationTest.prototype.takeCleanSnapshot = function () { + var cleanElement; + if (this.cleanId) { + cleanElement = document.getElementById(this.cleanId); + } + + // Enable clean page comparison element + if (cleanElement) { + this.enableDisplay(cleanElement); + } + + // Take a snapshot of the initial (clean) page + this.cleanSnapshot = snapshotWindow(window, false); + + // Disable the clean page comparison element + if (cleanElement) { + this.disableDisplay(cleanElement); + } + + var dataString1 = "Clean Snapshot"; + this.outputDebugInfo( + dataString1, + "cleanSnap", + this.cleanSnapshot.toDataURL() + ); +}; + +AnimationTest.prototype.takeBlankSnapshot = function () { + // Take a snapshot of the initial (essentially blank) page + this.blankSnapshot = snapshotWindow(window, false); + + var dataString1 = "Initial Blank Snapshot"; + this.outputDebugInfo( + dataString1, + "blank1Snap", + this.blankSnapshot.toDataURL() + ); +}; + +/** + * Begin the AnimationTest. This will utilize the information provided in the + * constructor to invoke a mochitest on animated images. It will automatically + * fail if allowed to run past the timeout. This will attempt to preload an + * image, if applicable, and then asynchronously call continueTest(), or if not + * applicable, synchronously trigger a call to continueTest(). + */ +AnimationTest.prototype.beginTest = function () { + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + + currentTest = this; + this.preloadImage(); +}; + +/** + * This is the second part of the test. It is triggered (eventually) from + * beginTest() either synchronously or asynchronously, as an image load + * callback. + */ +AnimationTest.prototype.continueTest = async function () { + // In case something goes wrong, fail earlier than mochitest timeout, + // and with more information. + setTimeout(failTest, this.timeout); + + if (!this.reusingImageAsReference) { + this.disableDisplay(document.getElementById(this.imageElementId)); + } + + let tookReference = new Promise(resolve => { + this.takeReferenceSnapshot(resolve); + }); + + tookReference.then(() => { + this.setupPolledImage(); + SimpleTest.executeSoon(pollForSuccess); + }); +}; + +AnimationTest.prototype.setupPolledImage = function () { + // Make sure the image is visible + if (!this.reusingImageAsReference) { + this.enableDisplay(document.getElementById(this.imageElementId)); + var currentSnapshot = snapshotWindow(window, false); + var result = compareSnapshots( + currentSnapshot, + this.referenceSnapshot, + true + ); + + this.currentSnapshotDataURI = currentSnapshot.toDataURL(); + + if (result[0]) { + // SUCCESS! + ok(true, "Animated image looks correct, at poll #" + this.pollCounter); + + this.outputDebugInfo( + "Animated image", + "animImage", + this.currentSnapshotDataURI + ); + + this.outputDebugInfo( + "Reference image", + "refImage", + this.referenceSnapshot.toDataURL() + ); + + this.cleanUpAndFinish(); + } + } else if (!gIsRefImageLoaded) { + this.myImage = new Image(); + this.myImage.onload = reuseImageCallback; + document + .getElementById(this.imageElementId) + .setAttribute("src", this.referenceElementId); + } +}; + +AnimationTest.prototype.checkImage = function () { + if (this.isTestFinished) { + return; + } + + this.pollCounter++; + + // We need this for some tests, because we need to force the + // test image to be visible. + if (!this.reusingImageAsReference) { + this.enableDisplay(document.getElementById(this.imageElementId)); + } + + var currentSnapshot = snapshotWindow(window, false); + var result = compareSnapshots(currentSnapshot, this.referenceSnapshot, true); + + this.currentSnapshotDataURI = currentSnapshot.toDataURL(); + + if (result[0]) { + // SUCCESS! + ok(true, "Animated image looks correct, at poll #" + this.pollCounter); + + this.outputDebugInfo("Animated image", "animImage", result[1]); + + this.outputDebugInfo("Reference image", "refImage", result[2]); + + this.cleanUpAndFinish(); + } +}; + +AnimationTest.prototype.takeReferenceSnapshot = function (resolve) { + this.numRefsTaken++; + + // Test to make sure the reference image doesn't match a clean snapshot + if (!this.cleanSnapshot) { + this.takeCleanSnapshot(); + } + + // Used later to verify that the reference div disappeared + if (!this.blankSnapshot) { + this.takeBlankSnapshot(); + } + + if (this.reusingImageAsReference) { + // Show reference elem (which is actually our image), & take a snapshot + var referenceElem = document.getElementById(this.imageElementId); + this.enableDisplay(referenceElem); + + this.referenceSnapshot = snapshotWindow(window, false); + + let snapResult = compareSnapshots( + this.cleanSnapshot, + this.referenceSnapshot, + false + ); + if (!snapResult[0]) { + if (this.blankWaitTime > 2000) { + // if it took longer than two seconds to load the image, we probably + // have a problem. + this.wereFailures = true; + ok( + snapResult[0], + "Reference snapshot shouldn't match clean (non-image) snapshot" + ); + } else { + this.blankWaitTime += currentTest.pollFreq; + // let's wait a bit and see if it clears up + setTimeout( + () => this.takeReferenceSnapshot(resolve), + currentTest.pollFreq + ); + return; + } + } + + ok( + snapResult[0], + "Reference snapshot shouldn't match clean (non-image) snapshot" + ); + + let dataString = "Reference Snapshot #" + this.numRefsTaken; + this.outputDebugInfo( + dataString, + "refSnapId", + this.referenceSnapshot.toDataURL() + ); + } else { + // Make sure the animation section is hidden + this.disableDisplay(document.getElementById(this.imageElementId)); + + // Show reference div, & take a snapshot + var referenceDiv = document.getElementById(this.referenceElementId); + this.enableDisplay(referenceDiv); + + this.referenceSnapshot = snapshotWindow(window, false); + let snapResult = compareSnapshots( + this.cleanSnapshot, + this.referenceSnapshot, + false + ); + if (!snapResult[0]) { + if (this.blankWaitTime > 2000) { + // if it took longer than two seconds to load the image, we probably + // have a problem. + this.wereFailures = true; + ok( + snapResult[0], + "Reference snapshot shouldn't match clean (non-image) snapshot" + ); + } else { + this.blankWaitTime += 20; + // let's wait a bit and see if it clears up + setTimeout(() => this.takeReferenceSnapshot(resolve), 20); + return; + } + } + + ok( + snapResult[0], + "Reference snapshot shouldn't match clean (non-image) snapshot" + ); + + let dataString = "Reference Snapshot #" + this.numRefsTaken; + this.outputDebugInfo( + dataString, + "refSnapId", + this.referenceSnapshot.toDataURL() + ); + + // Re-hide reference div, and take another snapshot to be sure it's gone + this.disableDisplay(referenceDiv); + this.testBlankCameBack(); + } + resolve(); +}; + +AnimationTest.prototype.enableDisplay = function (element) { + if (!element) { + return; + } + + if (!this.xulTest) { + element.style.display = ""; + } else { + element.setAttribute("hidden", "false"); + } +}; + +AnimationTest.prototype.disableDisplay = function (element) { + if (!element) { + return; + } + + if (!this.xulTest) { + element.style.display = "none"; + } else { + element.setAttribute("hidden", "true"); + } +}; + +AnimationTest.prototype.testBlankCameBack = function () { + var blankSnapshot2 = snapshotWindow(window, false); + var result = compareSnapshots(this.blankSnapshot, blankSnapshot2, true); + ok( + result[0], + "Reference image should disappear when it becomes display:none" + ); + + if (!result[0]) { + this.wereFailures = true; + var dataString = "Second Blank Snapshot"; + this.outputDebugInfo(dataString, "blank2SnapId", result[2]); + } +}; + +AnimationTest.prototype.cleanUpAndFinish = function () { + // On the off chance that failTest and checkImage are triggered + // back-to-back, use a flag to prevent multiple calls to SimpleTest.finish. + if (this.isTestFinished) { + return; + } + + this.isTestFinished = true; + + // Call our closing function, if one exists + if (this.closeFunc) { + this.closeFunc(); + return; + } + + if (this.wereFailures) { + document.getElementById(this.debugElementId).style.display = "block"; + } + + SimpleTest.finish(); + document.getElementById(this.debugElementId).style.display = ""; +}; diff --git a/image/test/mochitest/bad.jpg b/image/test/mochitest/bad.jpg new file mode 100644 index 0000000000..555a416d7d Binary files /dev/null and b/image/test/mochitest/bad.jpg differ diff --git a/image/test/mochitest/big.png b/image/test/mochitest/big.png new file mode 100644 index 0000000000..94e7eb6db2 Binary files /dev/null and b/image/test/mochitest/big.png differ diff --git a/image/test/mochitest/blue.gif b/image/test/mochitest/blue.gif new file mode 100644 index 0000000000..339f3702fb Binary files /dev/null and b/image/test/mochitest/blue.gif differ diff --git a/image/test/mochitest/blue.png b/image/test/mochitest/blue.png new file mode 100644 index 0000000000..8df58f3a5f Binary files /dev/null and b/image/test/mochitest/blue.png differ diff --git a/image/test/mochitest/bug1132427.gif b/image/test/mochitest/bug1132427.gif new file mode 100644 index 0000000000..39f49689a0 Binary files /dev/null and b/image/test/mochitest/bug1132427.gif differ diff --git a/image/test/mochitest/bug1132427.html b/image/test/mochitest/bug1132427.html new file mode 100644 index 0000000000..c765ce14ca --- /dev/null +++ b/image/test/mochitest/bug1132427.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/image/test/mochitest/bug1180105-waiter.sjs b/image/test/mochitest/bug1180105-waiter.sjs new file mode 100644 index 0000000000..4e20cb9976 --- /dev/null +++ b/image/test/mochitest/bug1180105-waiter.sjs @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var timer = Cc["@mozilla.org/timer;1"]; +var waitTimer = timer.createInstance(Ci.nsITimer); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.processAsync(); + waitForFinish(response); +} + +function waitForFinish(response) { + if (getSharedState("all-parts-done") === "1") { + response.write("done"); + response.finish(); + } else { + waitTimer.initWithCallback( + function () { + waitForFinish(response); + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } +} diff --git a/image/test/mochitest/bug1180105.sjs b/image/test/mochitest/bug1180105.sjs new file mode 100644 index 0000000000..35c7025508 --- /dev/null +++ b/image/test/mochitest/bug1180105.sjs @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var counter = 100; +var timer = Cc["@mozilla.org/timer;1"]; +var partTimer = timer.createInstance(Ci.nsITimer); + +function getFileAsInputStream(aFilename) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + file.append(aFilename); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function handleRequest(request, response) { + response.setHeader( + "Content-Type", + "multipart/x-mixed-replace;boundary=BOUNDARYOMG", + false + ); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + // We're sending parts off in a delayed fashion, to let the tests occur. + response.processAsync(); + response.write("--BOUNDARYOMG\r\n"); + sendParts(response); +} + +function sendParts(response) { + if (counter-- == 0) { + sendClose(response); + setSharedState("all-parts-done", "1"); + return; + } + sendNextPart(response); + partTimer.initWithCallback( + function () { + sendParts(response); + }, + 1, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} + +function sendClose(response) { + response.write("--BOUNDARYOMG--\r\n"); + response.finish(); +} + +function sendNextPart(response) { + var nextPartHead = "Content-Type: image/jpeg\r\n\r\n"; + var inputStream = getFileAsInputStream("damon.jpg"); + response.bodyOutputStream.write(nextPartHead, nextPartHead.length); + response.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + // Toss in the boundary, so the browser can know this part is complete + response.write("--BOUNDARYOMG\r\n"); +} diff --git a/image/test/mochitest/bug1217571-iframe.html b/image/test/mochitest/bug1217571-iframe.html new file mode 100644 index 0000000000..ab243d5cf5 --- /dev/null +++ b/image/test/mochitest/bug1217571-iframe.html @@ -0,0 +1,17 @@ + + + + + iframe for Bug 1217571 + + + + +

    + + + diff --git a/image/test/mochitest/bug1217571.jpg b/image/test/mochitest/bug1217571.jpg new file mode 100644 index 0000000000..917b336607 Binary files /dev/null and b/image/test/mochitest/bug1217571.jpg differ diff --git a/image/test/mochitest/bug1319025-ref.png b/image/test/mochitest/bug1319025-ref.png new file mode 100644 index 0000000000..482d027a02 Binary files /dev/null and b/image/test/mochitest/bug1319025-ref.png differ diff --git a/image/test/mochitest/bug1319025.png b/image/test/mochitest/bug1319025.png new file mode 100644 index 0000000000..8023e77879 Binary files /dev/null and b/image/test/mochitest/bug1319025.png differ diff --git a/image/test/mochitest/bug399925.gif b/image/test/mochitest/bug399925.gif new file mode 100644 index 0000000000..fc1c8f3af0 Binary files /dev/null and b/image/test/mochitest/bug399925.gif differ diff --git a/image/test/mochitest/bug415761.ico b/image/test/mochitest/bug415761.ico new file mode 100644 index 0000000000..d3f65abc23 Binary files /dev/null and b/image/test/mochitest/bug415761.ico differ diff --git a/image/test/mochitest/bug468160.sjs b/image/test/mochitest/bug468160.sjs new file mode 100644 index 0000000000..6495482922 --- /dev/null +++ b/image/test/mochitest/bug468160.sjs @@ -0,0 +1,5 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "red.png", false); + response.setHeader("Cache-Control", "no-cache", false); +} diff --git a/image/test/mochitest/bug478398_ONLY.png b/image/test/mochitest/bug478398_ONLY.png new file mode 100644 index 0000000000..e094ae2cf4 Binary files /dev/null and b/image/test/mochitest/bug478398_ONLY.png differ diff --git a/image/test/mochitest/bug490949-iframe.html b/image/test/mochitest/bug490949-iframe.html new file mode 100644 index 0000000000..68f74b587e --- /dev/null +++ b/image/test/mochitest/bug490949-iframe.html @@ -0,0 +1,7 @@ + + +Bug 490949 iframe + + + + diff --git a/image/test/mochitest/bug490949.sjs b/image/test/mochitest/bug490949.sjs new file mode 100644 index 0000000000..3b77dd9232 --- /dev/null +++ b/image/test/mochitest/bug490949.sjs @@ -0,0 +1,32 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + + var redirectstate = "/image/test/mochitest/bug490949.sjs"; + if (getState(redirectstate) == "") { + file.append("blue.png"); + setState(redirectstate, "red"); + } else { + file.append("red.png"); + setState(redirectstate, ""); + } + response.setHeader("Cache-Control", "no-cache", false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/image/test/mochitest/bug496292-1.sjs b/image/test/mochitest/bug496292-1.sjs new file mode 100644 index 0000000000..2e842223d5 --- /dev/null +++ b/image/test/mochitest/bug496292-1.sjs @@ -0,0 +1,31 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + + if (request.getHeader("Accept") == "image/avif,image/webp,*/*") { + file.append("blue.png"); + } else { + file.append("red.png"); + } + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Cache-Control", "no-cache", false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/image/test/mochitest/bug496292-2.sjs b/image/test/mochitest/bug496292-2.sjs new file mode 100644 index 0000000000..af174e99c4 --- /dev/null +++ b/image/test/mochitest/bug496292-2.sjs @@ -0,0 +1,31 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + + if (request.getHeader("Accept") == "image/png") { + file.append("blue.png"); + } else { + file.append("red.png"); + } + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Cache-Control", "no-cache", false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/image/test/mochitest/bug496292-iframe-1.html b/image/test/mochitest/bug496292-iframe-1.html new file mode 100644 index 0000000000..00f0fbcfce --- /dev/null +++ b/image/test/mochitest/bug496292-iframe-1.html @@ -0,0 +1,7 @@ + + +Bug 496292 iframe 1 + + + + diff --git a/image/test/mochitest/bug496292-iframe-2.html b/image/test/mochitest/bug496292-iframe-2.html new file mode 100644 index 0000000000..67c1ecea16 --- /dev/null +++ b/image/test/mochitest/bug496292-iframe-2.html @@ -0,0 +1,7 @@ + + +Bug 496292 iframe 2 + + + + diff --git a/image/test/mochitest/bug496292-iframe-ref.html b/image/test/mochitest/bug496292-iframe-ref.html new file mode 100644 index 0000000000..2e804502e6 --- /dev/null +++ b/image/test/mochitest/bug496292-iframe-ref.html @@ -0,0 +1,7 @@ + + +Bug 496292 reference iframe + + + + diff --git a/image/test/mochitest/bug497665-iframe.html b/image/test/mochitest/bug497665-iframe.html new file mode 100644 index 0000000000..a2b098e31b --- /dev/null +++ b/image/test/mochitest/bug497665-iframe.html @@ -0,0 +1,8 @@ + + +Bug 497665 iframe + + + + + diff --git a/image/test/mochitest/bug497665.sjs b/image/test/mochitest/bug497665.sjs new file mode 100644 index 0000000000..cc6361d7c5 --- /dev/null +++ b/image/test/mochitest/bug497665.sjs @@ -0,0 +1,33 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + + var redirectstate = "/image/test/mochitest/bug497665.sjs"; + if (getState(redirectstate) == "") { + file.append("blue.png"); + setState(redirectstate, "red"); + } else { + file.append("red.png"); + setState(redirectstate, ""); + } + + response.setHeader("Cache-Control", "max-age=3600", false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/image/test/mochitest/bug552605.sjs b/image/test/mochitest/bug552605.sjs new file mode 100644 index 0000000000..350ab729d5 --- /dev/null +++ b/image/test/mochitest/bug552605.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + var redirectstate = "/image/test/mochitest/bug89419.sjs"; + response.setStatusLine("1.1", 302, "Found"); + if (getState(redirectstate) == "") { + response.setHeader("Location", "red.png", false); + setState(redirectstate, "red"); + } else { + response.setHeader("Location", "blue.png", false); + setState(redirectstate, ""); + } + response.setHeader("Cache-Control", "no-cache", false); +} diff --git a/image/test/mochitest/bug657191.sjs b/image/test/mochitest/bug657191.sjs new file mode 100644 index 0000000000..6a2c7924e1 --- /dev/null +++ b/image/test/mochitest/bug657191.sjs @@ -0,0 +1,26 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + file.append("lime100x100.svg"); + + response.setStatusLine("1.1", 500, "Internal Server Error"); + response.setHeader("Content-Type", "image/svg+xml", false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/image/test/mochitest/bug671906-iframe.html b/image/test/mochitest/bug671906-iframe.html new file mode 100644 index 0000000000..87f8183a4d --- /dev/null +++ b/image/test/mochitest/bug671906-iframe.html @@ -0,0 +1,7 @@ + + +Bug 671906 iframe + + + + diff --git a/image/test/mochitest/bug671906.sjs b/image/test/mochitest/bug671906.sjs new file mode 100644 index 0000000000..8473e87f3a --- /dev/null +++ b/image/test/mochitest/bug671906.sjs @@ -0,0 +1,34 @@ +function handleRequest(request, response) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + + var filestate = "/image/test/mochitest/bug671906.sjs"; + if (getState(filestate) == "") { + file.append("blue.png"); + setState(filestate, "red"); + } else { + file.append("red.png"); + setState(filestate, ""); + } + + // Set the expires date to some silly time in the future so we're sure to + // *want* to cache this image. + var date = new Date(); + date.setFullYear(date.getFullYear() + 1); + response.setHeader("Expires", date.toUTCString(), false); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + + response.bodyOutputStream.writeFrom(fileStream, fileStream.available()); + + fileStream.close(); + + response.setHeader("Access-Control-Allow-Origin", "*", false); +} diff --git a/image/test/mochitest/bug733553-informant.sjs b/image/test/mochitest/bug733553-informant.sjs new file mode 100644 index 0000000000..364830b636 --- /dev/null +++ b/image/test/mochitest/bug733553-informant.sjs @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + // Tells bug733553.sjs that the consumer is ready for the next part + let partName = request.queryString; + setSharedState("next-part", partName); + response.write("OK!"); +} diff --git a/image/test/mochitest/bug733553.sjs b/image/test/mochitest/bug733553.sjs new file mode 100644 index 0000000000..869477b581 --- /dev/null +++ b/image/test/mochitest/bug733553.sjs @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var bodyPartIndex = -1; +var bodyParts = [ + ["red.png", "image/png"], + ["animated-gif2.gif", "image/gif"], + ["red.png", "image/png"], + ["lime100x100.svg", "image/svg+xml"], + ["lime100x100.svg", "image/svg+xml"], + ["animated-gif2.gif", "image/gif"], + ["red.png", "image/png"], + // Mime type intentionally wrong (test for bug 907575) + ["shaver.png", "image/gif"], + ["red.png", "image/png"], + ["damon.jpg", "image/jpeg"], + ["damon.jpg", "application/octet-stream"], + ["damon.jpg", "image/jpeg"], + ["rillybad.jpg", "application/x-unknown-content-type"], + ["damon.jpg", "image/jpeg"], + ["bad.jpg", "image/jpeg"], + ["red.png", "image/png"], + ["invalid.jpg", "image/jpeg"], + ["animated-gif2.gif", "image/gif"], +]; +var timer = Cc["@mozilla.org/timer;1"]; +var partTimer = timer.createInstance(Ci.nsITimer); + +function getFileAsInputStream(aFilename) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + file.append(aFilename); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function handleRequest(request, response) { + if (!getSharedState("next-part")) { + setSharedState("next-part", "-1"); + } + response.setHeader( + "Content-Type", + "multipart/x-mixed-replace;boundary=BOUNDARYOMG", + false + ); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + // We're sending parts off in a delayed fashion, to let the tests occur. + response.processAsync(); + response.write("--BOUNDARYOMG\r\n"); + sendParts(response); +} + +function sendParts(response) { + let wait = false; + let nextPart = parseInt(getSharedState("next-part"), 10); + if (nextPart == bodyPartIndex) { + // Haven't been signaled yet, remain in holding pattern + wait = true; + } else { + bodyPartIndex = nextPart; + } + if (bodyParts.length > bodyPartIndex) { + let callback; + if (!wait) { + callback = getSendNextPart(response); + } else { + callback = function () { + sendParts(response); + }; + } + partTimer.initWithCallback(callback, 1000, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + sendClose(response); + } +} + +function sendClose(response) { + response.write("--BOUNDARYOMG--\r\n"); + response.finish(); +} + +function getSendNextPart(response) { + var part = bodyParts[bodyPartIndex]; + var nextPartHead = "Content-Type: " + part[1] + "\r\n\r\n"; + var inputStream = getFileAsInputStream(part[0]); + return function () { + response.bodyOutputStream.write(nextPartHead, nextPartHead.length); + response.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + // Toss in the boundary, so the browser can know this part is complete + response.write("--BOUNDARYOMG\r\n"); + sendParts(response); + }; +} diff --git a/image/test/mochitest/bug767779.sjs b/image/test/mochitest/bug767779.sjs new file mode 100644 index 0000000000..b29b00cf1c --- /dev/null +++ b/image/test/mochitest/bug767779.sjs @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var timer = Cc["@mozilla.org/timer;1"]; +var partTimer = timer.createInstance(Ci.nsITimer); + +function getFileAsInputStream(aFilename) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + file.append(aFilename); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function handleRequest(request, response) { + response.setHeader("Content-Type", "image/gif", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + // We're sending data off in a delayed fashion + response.processAsync(); + var inputStream = getFileAsInputStream("animated-gif_trailing-garbage.gif"); + // Should be 4029 bytes available. + // Send the good data at once + response.bodyOutputStream.writeFrom(inputStream, 285); + sendParts(inputStream, response); +} + +function sendParts(inputStream, response) { + // 3744 left, send in 8 chunks of 468 each + partTimer.initWithCallback( + getSendNextPart(inputStream, response), + 500, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} + +function getSendNextPart(inputStream, response) { + return function () { + response.bodyOutputStream.writeFrom(inputStream, 468); + if (!inputStream.available()) { + inputStream.close(); + response.finish(); + } else { + sendParts(inputStream, response); + } + }; +} diff --git a/image/test/mochitest/bug89419-iframe.html b/image/test/mochitest/bug89419-iframe.html new file mode 100644 index 0000000000..1915315633 --- /dev/null +++ b/image/test/mochitest/bug89419-iframe.html @@ -0,0 +1,7 @@ + + +Bug 89419 iframe + + + + diff --git a/image/test/mochitest/bug89419.sjs b/image/test/mochitest/bug89419.sjs new file mode 100644 index 0000000000..350ab729d5 --- /dev/null +++ b/image/test/mochitest/bug89419.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + var redirectstate = "/image/test/mochitest/bug89419.sjs"; + response.setStatusLine("1.1", 302, "Found"); + if (getState(redirectstate) == "") { + response.setHeader("Location", "red.png", false); + setState(redirectstate, "red"); + } else { + response.setHeader("Location", "blue.png", false); + setState(redirectstate, ""); + } + response.setHeader("Cache-Control", "no-cache", false); +} diff --git a/image/test/mochitest/bug900200-ref.png b/image/test/mochitest/bug900200-ref.png new file mode 100644 index 0000000000..6360131325 Binary files /dev/null and b/image/test/mochitest/bug900200-ref.png differ diff --git a/image/test/mochitest/bug900200.png b/image/test/mochitest/bug900200.png new file mode 100644 index 0000000000..d7d87adce0 Binary files /dev/null and b/image/test/mochitest/bug900200.png differ diff --git a/image/test/mochitest/child.html b/image/test/mochitest/child.html new file mode 100644 index 0000000000..19f262ec3a --- /dev/null +++ b/image/test/mochitest/child.html @@ -0,0 +1,22 @@ + + + + + diff --git a/image/test/mochitest/chrome.ini b/image/test/mochitest/chrome.ini new file mode 100644 index 0000000000..b84d0af897 --- /dev/null +++ b/image/test/mochitest/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] +skip-if = os == 'android' + +[test_bug415761.html] +skip-if = os != "win" || os_version == "6.2" +support-files = + bug415761.ico diff --git a/image/test/mochitest/clear.avif b/image/test/mochitest/clear.avif new file mode 100644 index 0000000000..b68e5729b4 Binary files /dev/null and b/image/test/mochitest/clear.avif differ diff --git a/image/test/mochitest/clear.gif b/image/test/mochitest/clear.gif new file mode 100644 index 0000000000..7ae79ba86e Binary files /dev/null and b/image/test/mochitest/clear.gif differ diff --git a/image/test/mochitest/clear.png b/image/test/mochitest/clear.png new file mode 100644 index 0000000000..b09aecaaa0 Binary files /dev/null and b/image/test/mochitest/clear.png differ diff --git a/image/test/mochitest/clear.webp b/image/test/mochitest/clear.webp new file mode 100644 index 0000000000..6db376d6e1 Binary files /dev/null and b/image/test/mochitest/clear.webp differ diff --git a/image/test/mochitest/clear2-results.gif b/image/test/mochitest/clear2-results.gif new file mode 100644 index 0000000000..965b650253 Binary files /dev/null and b/image/test/mochitest/clear2-results.gif differ diff --git a/image/test/mochitest/clear2.gif b/image/test/mochitest/clear2.gif new file mode 100644 index 0000000000..00ad873c65 Binary files /dev/null and b/image/test/mochitest/clear2.gif differ diff --git a/image/test/mochitest/clear2.webp b/image/test/mochitest/clear2.webp new file mode 100644 index 0000000000..e4a3e2efab Binary files /dev/null and b/image/test/mochitest/clear2.webp differ diff --git a/image/test/mochitest/damon.jpg b/image/test/mochitest/damon.jpg new file mode 100644 index 0000000000..917b336607 Binary files /dev/null and b/image/test/mochitest/damon.jpg differ diff --git a/image/test/mochitest/error-early.png b/image/test/mochitest/error-early.png new file mode 100644 index 0000000000..5df7507e2d --- /dev/null +++ b/image/test/mochitest/error-early.png @@ -0,0 +1 @@ +ERROR diff --git a/image/test/mochitest/filter-final.svg b/image/test/mochitest/filter-final.svg new file mode 100644 index 0000000000..b2b3dca008 --- /dev/null +++ b/image/test/mochitest/filter-final.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/image/test/mochitest/filter.svg b/image/test/mochitest/filter.svg new file mode 100644 index 0000000000..e185f15b69 --- /dev/null +++ b/image/test/mochitest/filter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/image/test/mochitest/finite-apng.png b/image/test/mochitest/finite-apng.png new file mode 100644 index 0000000000..778613d851 Binary files /dev/null and b/image/test/mochitest/finite-apng.png differ diff --git a/image/test/mochitest/first-frame-padding.gif b/image/test/mochitest/first-frame-padding.gif new file mode 100644 index 0000000000..e6d7c49322 Binary files /dev/null and b/image/test/mochitest/first-frame-padding.gif differ diff --git a/image/test/mochitest/green-background.html b/image/test/mochitest/green-background.html new file mode 100644 index 0000000000..731919f766 --- /dev/null +++ b/image/test/mochitest/green-background.html @@ -0,0 +1,30 @@ + + + +Background color wrapper for clear image tests + + + + + + + + diff --git a/image/test/mochitest/green.png b/image/test/mochitest/green.png new file mode 100644 index 0000000000..7df25f33bd Binary files /dev/null and b/image/test/mochitest/green.png differ diff --git a/image/test/mochitest/grey.png b/image/test/mochitest/grey.png new file mode 100644 index 0000000000..5c82cdeb10 Binary files /dev/null and b/image/test/mochitest/grey.png differ diff --git a/image/test/mochitest/ico-bmp-opaque.ico b/image/test/mochitest/ico-bmp-opaque.ico new file mode 100644 index 0000000000..3cf3320eae Binary files /dev/null and b/image/test/mochitest/ico-bmp-opaque.ico differ diff --git a/image/test/mochitest/ico-bmp-transparent.ico b/image/test/mochitest/ico-bmp-transparent.ico new file mode 100644 index 0000000000..151b7cb361 Binary files /dev/null and b/image/test/mochitest/ico-bmp-transparent.ico differ diff --git a/image/test/mochitest/iframe.html b/image/test/mochitest/iframe.html new file mode 100644 index 0000000000..6d66557ef8 --- /dev/null +++ b/image/test/mochitest/iframe.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/image/test/mochitest/imgutils.js b/image/test/mochitest/imgutils.js new file mode 100644 index 0000000000..b16ad1d065 --- /dev/null +++ b/image/test/mochitest/imgutils.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +// Helper file for shared image functionality +// +// Note that this is use by tests elsewhere in the source tree. When in doubt, +// check mxr before removing or changing functionality. + +// Helper function to clear both the content and chrome image caches +function clearAllImageCaches() { + var tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + var imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content +} + +// Helper function to clear the image cache of content images +function clearImageCache() { + var tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + var imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(false); // true=chrome, false=content +} + +// Helper function to determine if the frame is decoded for a given image id +function isFrameDecoded(id) { + return !!( + getImageStatus(id) & SpecialPowers.Ci.imgIRequest.STATUS_FRAME_COMPLETE + ); +} + +// Helper function to determine if the image is loaded for a given image id +function isImageLoaded(id) { + return !!( + getImageStatus(id) & SpecialPowers.Ci.imgIRequest.STATUS_LOAD_COMPLETE + ); +} + +// Helper function to get the status flags of an image +function getImageStatus(id) { + // Get the image + var img = SpecialPowers.wrap(document.getElementById(id)); + + // Get the request + var request = img.getRequest( + SpecialPowers.Ci.nsIImageLoadingContent.CURRENT_REQUEST + ); + + // Return the status + return request.imageStatus; +} + +// Forces a synchronous decode of an image by drawing it to a canvas. Only +// really meaningful if the image is fully loaded first +function forceDecode(id) { + // Get the image + var img = document.getElementById(id); + + // Make a new canvas + var canvas = document.createElement("canvas"); + + // Draw the image to the canvas. This forces a synchronous decode + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); +} + +// Functions to facilitate getting/setting various image-related prefs +// +// If you change a pref in a mochitest, Don't forget to reset it to its +// original value! +// +// Null indicates no pref set + +const DISCARD_ENABLED_PREF = { + name: "discardable", + branch: "image.mem.", + type: "bool", +}; +const DECODEONDRAW_ENABLED_PREF = { + name: "decodeondraw", + branch: "image.mem.", + type: "bool", +}; +const DISCARD_TIMEOUT_PREF = { + name: "min_discard_timeout_ms", + branch: "image.mem.", + type: "int", +}; + +function setImagePref(pref, val) { + var prefService = SpecialPowers.Services.prefs; + var branch = prefService.getBranch(pref.branch); + if (val != null) { + switch (pref.type) { + case "bool": + branch.setBoolPref(pref.name, val); + break; + case "int": + branch.setIntPref(pref.name, val); + break; + default: + throw new Error("Unknown pref type"); + } + } else if (branch.prefHasUserValue(pref.name)) { + branch.clearUserPref(pref.name); + } +} + +function getImagePref(pref) { + var prefService = SpecialPowers.Services.prefs; + var branch = prefService.getBranch(pref.branch); + if (branch.prefHasUserValue(pref.name)) { + switch (pref.type) { + case "bool": + return branch.getBoolPref(pref.name); + case "int": + return branch.getIntPref(pref.name); + default: + throw new Error("Unknown pref type"); + } + } else { + return null; + } +} + +// JS implementation of imgIScriptedNotificationObserver with stubs for all of its methods. +function ImageDecoderObserverStub() { + this.sizeAvailable = function sizeAvailable(aRequest) {}; + this.frameComplete = function frameComplete(aRequest) {}; + this.decodeComplete = function decodeComplete(aRequest) {}; + this.loadComplete = function loadComplete(aRequest) {}; + this.frameUpdate = function frameUpdate(aRequest) {}; + this.discard = function discard(aRequest) {}; + this.isAnimated = function isAnimated(aRequest) {}; + this.hasTransparency = function hasTransparency(aRequest) {}; +} diff --git a/image/test/mochitest/infinite-apng.png b/image/test/mochitest/infinite-apng.png new file mode 100644 index 0000000000..637dafbc2b Binary files /dev/null and b/image/test/mochitest/infinite-apng.png differ diff --git a/image/test/mochitest/infinite.avif b/image/test/mochitest/infinite.avif new file mode 100644 index 0000000000..a5be1912a8 Binary files /dev/null and b/image/test/mochitest/infinite.avif differ diff --git a/image/test/mochitest/infinite.webp b/image/test/mochitest/infinite.webp new file mode 100644 index 0000000000..4219e179b0 Binary files /dev/null and b/image/test/mochitest/infinite.webp differ diff --git a/image/test/mochitest/invalid.jpg b/image/test/mochitest/invalid.jpg new file mode 100644 index 0000000000..c677a81e29 --- /dev/null +++ b/image/test/mochitest/invalid.jpg @@ -0,0 +1 @@ +notajpg diff --git a/image/test/mochitest/keep.gif b/image/test/mochitest/keep.gif new file mode 100644 index 0000000000..e967d6a6dc Binary files /dev/null and b/image/test/mochitest/keep.gif differ diff --git a/image/test/mochitest/keep.png b/image/test/mochitest/keep.png new file mode 100644 index 0000000000..aa3ff74450 Binary files /dev/null and b/image/test/mochitest/keep.png differ diff --git a/image/test/mochitest/keep.webp b/image/test/mochitest/keep.webp new file mode 100644 index 0000000000..342982be06 Binary files /dev/null and b/image/test/mochitest/keep.webp differ diff --git a/image/test/mochitest/lime-anim-100x100-2.svg b/image/test/mochitest/lime-anim-100x100-2.svg new file mode 100644 index 0000000000..d19d3b0e7e --- /dev/null +++ b/image/test/mochitest/lime-anim-100x100-2.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/image/test/mochitest/lime-anim-100x100.svg b/image/test/mochitest/lime-anim-100x100.svg new file mode 100644 index 0000000000..c6584047d0 --- /dev/null +++ b/image/test/mochitest/lime-anim-100x100.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/image/test/mochitest/lime-css-anim-100x100.svg b/image/test/mochitest/lime-css-anim-100x100.svg new file mode 100644 index 0000000000..3edbd3eaaf --- /dev/null +++ b/image/test/mochitest/lime-css-anim-100x100.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/image/test/mochitest/lime100x100.svg b/image/test/mochitest/lime100x100.svg new file mode 100644 index 0000000000..8bdec62c1f --- /dev/null +++ b/image/test/mochitest/lime100x100.svg @@ -0,0 +1,4 @@ + + + diff --git a/image/test/mochitest/mochitest.ini b/image/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..9687c5d8a5 --- /dev/null +++ b/image/test/mochitest/mochitest.ini @@ -0,0 +1,184 @@ +[DEFAULT] +support-files = + INT32_MIN.bmp + animated1.gif + animated1.svg + animated2.gif + animatedMask.gif + animated-gif.gif + animated-gif2.gif + animated-gif_trailing-garbage.gif + animated-gif-finalframe.gif + animated-avif.avif + animation.svg + animationPolling.js + bad.jpg + big.png + blue.gif + blue.png + bug399925.gif + bug468160.sjs + bug478398_ONLY.png + bug490949-iframe.html + bug490949.sjs + bug496292-1.sjs + bug496292-2.sjs + bug496292-iframe-1.html + bug496292-iframe-2.html + bug496292-iframe-ref.html + bug497665-iframe.html + bug497665.sjs + bug552605.sjs + bug657191.sjs + bug671906-iframe.html + bug671906.sjs + bug733553-informant.sjs + bug733553.sjs + bug767779.sjs + bug89419-iframe.html + bug89419.sjs + bug900200.png + bug900200-ref.png + bug1132427.html + bug1132427.gif + bug1180105.sjs + bug1180105-waiter.sjs + bug1217571-iframe.html + bug1217571.jpg + bug1319025.png + bug1319025-ref.png + clear.gif + clear.png + clear.webp + clear.avif + clear2.gif + clear2.webp + clear2-results.gif + damon.jpg + error-early.png + filter-final.svg + filter.svg + finite-apng.png + first-frame-padding.gif + green.png + green-background.html + grey.png + ico-bmp-opaque.ico + ico-bmp-transparent.ico + iframe.html + imgutils.js + infinite.avif + infinite.webp + infinite-apng.png + invalid.jpg + keep.gif + keep.png + keep.webp + lime100x100.svg + lime-anim-100x100.svg + lime-anim-100x100-2.svg + lime-css-anim-100x100.svg + mq_dynamic_svg_test.html + mq_dynamic_svg_ref.html + opaque.bmp + purple.gif + rainbow.gif + red.gif + red.png + ref-iframe.html + restore-previous.gif + restore-previous.png + rillybad.jpg + schrep.png + shaver.png + short_header.gif + source.png + transparent.gif + transparent.png + over.png + webcam-simulacrum.sjs + 6M-pixels.png + 12M-pixels-1.png + 12M-pixels-2.png + +[test_animated_css_image.html] +[test_animated_gif.html] +support-files = child.html +skip-if = + http3 +[test_animation.html] +[test_canvas_frame_animation.html] +[test_animation_operators.html] +[test_animation2.html] +[test_animSVGImage.html] +skip-if = os == 'android' || os == 'win' || (os == 'mac' && os_version == '10.15') # Bug 1370784, macosx due to bug 1549058 +[test_animSVGImage2.html] +skip-if = + os == 'win' # Bug 1354561 + os == 'linux' # Bug 1354561 + os == 'android' # Bug 1354561 +[test_background_image_anim.html] +[test_bug399925.html] +[test_bug435296.html] +skip-if = true # disabled - See bug 578591 +[test_bug466586.html] +[test_bug468160.html] +[test_bug478398.html] +skip-if = true # disabled - See bug 579139 +[test_bug490949.html] +[test_bug496292.html] +skip-if = verify +[test_bug497665.html] +[test_bug552605-1.html] +[test_bug552605-2.html] +[test_bug553982.html] +[test_bug601470.html] +[test_bug614392.html] +[test_bug657191.html] +[test_bug671906.html] +skip-if = + http3 +[test_bug733553.html] +skip-if = + verify + http3 +[test_bug767779.html] +[test_bug865919.html] +[test_bug89419-1.html] +[test_bug89419-2.html] +[test_bug1132427.html] +[test_bug1180105.html] +skip-if = + http3 +[test_bug1217571.html] +[test_bug1325080.html] +[test_bullet_animation.html] +[test_changeOfSource.html] +[test_changeOfSource2.html] +[test_discardAnimatedImage.html] +skip-if = + http3 +[test_discardFinishedAnimatedImage.html] +[test_discardFramesAnimatedImage.html] +[test_drawDiscardedImage.html] +[test_error_events.html] +[test_image_crossorigin_data_url.html] +[test_has_transparency.html] +[test_mq_dynamic_svg.html] +[test_net_failedtoprocess.html] +skip-if = verify +[test_removal_ondecode.html] +[test_removal_onload.html] +[test_short_gif_header.html] +[test_staticClone.html] +[test_svg_animatedGIF.html] +[test_svg_filter_animation.html] +[test_synchronized_animation.html] +disabled = bug 1295501 +[test_undisplayed_iframe.html] +[test_webcam.html] +[test_xultree_animation.xhtml] +allow_xul_xbl = true +skip-if = + http3 +[test_image_cache_notification.html] diff --git a/image/test/mochitest/mq_dynamic_svg_ref.html b/image/test/mochitest/mq_dynamic_svg_ref.html new file mode 100644 index 0000000000..bbd4a3e205 --- /dev/null +++ b/image/test/mochitest/mq_dynamic_svg_ref.html @@ -0,0 +1,38 @@ + + + + +

    inline

    + + + +

    iframe

    + +

    img

    + +

    background-image

    +
    +

    img with nested document

    + diff --git a/image/test/mochitest/mq_dynamic_svg_test.html b/image/test/mochitest/mq_dynamic_svg_test.html new file mode 100644 index 0000000000..8acb6d3331 --- /dev/null +++ b/image/test/mochitest/mq_dynamic_svg_test.html @@ -0,0 +1,61 @@ + + + + +

    inline

    + + + +

    iframe

    + +

    img

    + +

    background-image

    +
    +

    img with nested document

    + diff --git a/image/test/mochitest/opaque.bmp b/image/test/mochitest/opaque.bmp new file mode 100644 index 0000000000..63d3f1c058 Binary files /dev/null and b/image/test/mochitest/opaque.bmp differ diff --git a/image/test/mochitest/over.png b/image/test/mochitest/over.png new file mode 100644 index 0000000000..9e957182f7 Binary files /dev/null and b/image/test/mochitest/over.png differ diff --git a/image/test/mochitest/purple.gif b/image/test/mochitest/purple.gif new file mode 100644 index 0000000000..79826af205 Binary files /dev/null and b/image/test/mochitest/purple.gif differ diff --git a/image/test/mochitest/rainbow.gif b/image/test/mochitest/rainbow.gif new file mode 100644 index 0000000000..a247a80df0 Binary files /dev/null and b/image/test/mochitest/rainbow.gif differ diff --git a/image/test/mochitest/red.gif b/image/test/mochitest/red.gif new file mode 100644 index 0000000000..d3c32bae25 Binary files /dev/null and b/image/test/mochitest/red.gif differ diff --git a/image/test/mochitest/red.png b/image/test/mochitest/red.png new file mode 100644 index 0000000000..aa9ce25263 Binary files /dev/null and b/image/test/mochitest/red.png differ diff --git a/image/test/mochitest/ref-iframe.html b/image/test/mochitest/ref-iframe.html new file mode 100644 index 0000000000..585772c8a9 --- /dev/null +++ b/image/test/mochitest/ref-iframe.html @@ -0,0 +1,6 @@ + + +
    + + diff --git a/image/test/mochitest/restore-previous.gif b/image/test/mochitest/restore-previous.gif new file mode 100644 index 0000000000..15ba9ddc48 Binary files /dev/null and b/image/test/mochitest/restore-previous.gif differ diff --git a/image/test/mochitest/restore-previous.png b/image/test/mochitest/restore-previous.png new file mode 100644 index 0000000000..09dee63820 Binary files /dev/null and b/image/test/mochitest/restore-previous.png differ diff --git a/image/test/mochitest/rillybad.jpg b/image/test/mochitest/rillybad.jpg new file mode 100644 index 0000000000..e2fb1d303f Binary files /dev/null and b/image/test/mochitest/rillybad.jpg differ diff --git a/image/test/mochitest/schrep.png b/image/test/mochitest/schrep.png new file mode 100644 index 0000000000..bcb406387d Binary files /dev/null and b/image/test/mochitest/schrep.png differ diff --git a/image/test/mochitest/shaver.png b/image/test/mochitest/shaver.png new file mode 100644 index 0000000000..ab0b6c7b40 Binary files /dev/null and b/image/test/mochitest/shaver.png differ diff --git a/image/test/mochitest/short_header.gif b/image/test/mochitest/short_header.gif new file mode 100644 index 0000000000..70af95ac6d Binary files /dev/null and b/image/test/mochitest/short_header.gif differ diff --git a/image/test/mochitest/source.png b/image/test/mochitest/source.png new file mode 100644 index 0000000000..df1c76dae5 Binary files /dev/null and b/image/test/mochitest/source.png differ diff --git a/image/test/mochitest/test_animSVGImage.html b/image/test/mochitest/test_animSVGImage.html new file mode 100644 index 0000000000..a405cdd46b --- /dev/null +++ b/image/test/mochitest/test_animSVGImage.html @@ -0,0 +1,124 @@ + + + + + Test for Bug 610419 + + + + + + +Mozilla Bug 610419 +

    +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_animSVGImage2.html b/image/test/mochitest/test_animSVGImage2.html new file mode 100644 index 0000000000..0f3ae046c5 --- /dev/null +++ b/image/test/mochitest/test_animSVGImage2.html @@ -0,0 +1,124 @@ + + + + + Test for Bug 907503 + + + + + + +Mozilla Bug 907503 +

    +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_animated_css_image.html b/image/test/mochitest/test_animated_css_image.html new file mode 100644 index 0000000000..ca4a47915f --- /dev/null +++ b/image/test/mochitest/test_animated_css_image.html @@ -0,0 +1,223 @@ + + + + + + diff --git a/image/test/mochitest/test_animated_gif.html b/image/test/mochitest/test_animated_gif.html new file mode 100644 index 0000000000..814749b7f0 --- /dev/null +++ b/image/test/mochitest/test_animated_gif.html @@ -0,0 +1,50 @@ + + + + +Images outside of display port are not decoded + + + + + +

    + +
    +
    + +
    +
    +
    
    +
    +
    +
    diff --git a/image/test/mochitest/test_animation.html b/image/test/mochitest/test_animation.html
    new file mode 100644
    index 0000000000..725cd93e85
    --- /dev/null
    +++ b/image/test/mochitest/test_animation.html
    @@ -0,0 +1,45 @@
    +
    +
    +
    +
    +  Test for Bug 666446 - General Animated GIF Test
    +  
    +  
    +  
    +  
    +  
    +
    +
    +
    +Mozilla Bug 666446: lots of animated gifs swamp us with paint events
    +
    +

    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_animation2.html b/image/test/mochitest/test_animation2.html new file mode 100644 index 0000000000..1b4e0f94c7 --- /dev/null +++ b/image/test/mochitest/test_animation2.html @@ -0,0 +1,49 @@ + + + + + Test for Bug 705580 - General Animated GIF Test 2 + + + + + + + + +Mozilla Bug 705580: Test animated GIFs that are converted to ImageLayers + +

    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_animation_operators.html b/image/test/mochitest/test_animation_operators.html new file mode 100644 index 0000000000..faece37053 --- /dev/null +++ b/image/test/mochitest/test_animation_operators.html @@ -0,0 +1,168 @@ + + + + + Test for Bug 936720 + + + + + +Mozilla Bug 936720 +
    +
    +
    + + diff --git a/image/test/mochitest/test_background_image_anim.html b/image/test/mochitest/test_background_image_anim.html new file mode 100644 index 0000000000..f1aeb6288b --- /dev/null +++ b/image/test/mochitest/test_background_image_anim.html @@ -0,0 +1,44 @@ + + + + + Test for Bug 666446 - Animated Background Images + + + + + + + + +Mozilla Bug 666446: lots of animated gifs swamp us with paint events + +

    +
    +
    + +
    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug1132427.html b/image/test/mochitest/test_bug1132427.html new file mode 100644 index 0000000000..0ee2872fea --- /dev/null +++ b/image/test/mochitest/test_bug1132427.html @@ -0,0 +1,94 @@ + + + + Test for scrolling selection into view + + + + + + +
    +
    +
    + + + diff --git a/image/test/mochitest/test_bug1180105.html b/image/test/mochitest/test_bug1180105.html new file mode 100644 index 0000000000..579c8db760 --- /dev/null +++ b/image/test/mochitest/test_bug1180105.html @@ -0,0 +1,46 @@ + + + + + Test for Bug 1180105 + + + + + +Mozilla Bug 1180105 +

    +
    +
    +
    +
    > + +
    + + diff --git a/image/test/mochitest/test_bug1217571.html b/image/test/mochitest/test_bug1217571.html new file mode 100644 index 0000000000..f81fc7c51d --- /dev/null +++ b/image/test/mochitest/test_bug1217571.html @@ -0,0 +1,43 @@ + + + + + Test for Bug 1217571 + + + + +Mozilla Bug 1217571 +

    + + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug1325080.html b/image/test/mochitest/test_bug1325080.html new file mode 100644 index 0000000000..91dea11173 --- /dev/null +++ b/image/test/mochitest/test_bug1325080.html @@ -0,0 +1,37 @@ + + + + + Test for Bug 1325080 + + + + +Mozilla Bug 1325080 +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug399925.html b/image/test/mochitest/test_bug399925.html new file mode 100644 index 0000000000..ae45479377 --- /dev/null +++ b/image/test/mochitest/test_bug399925.html @@ -0,0 +1,102 @@ + + + + + Test for Bug 399925 + + + + + +Mozilla Bug 399925 +

    + +
    +
    +
    + + + diff --git a/image/test/mochitest/test_bug415761.html b/image/test/mochitest/test_bug415761.html new file mode 100644 index 0000000000..f3bf6c67a8 --- /dev/null +++ b/image/test/mochitest/test_bug415761.html @@ -0,0 +1,117 @@ + + + + Test for icon filenames + + + + + + +
    +
    +
    + + + + diff --git a/image/test/mochitest/test_bug435296.html b/image/test/mochitest/test_bug435296.html new file mode 100644 index 0000000000..1610410b16 --- /dev/null +++ b/image/test/mochitest/test_bug435296.html @@ -0,0 +1,85 @@ + + + + + Test for Bug 435296 + + + + + + +Mozilla Bug 435296 + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug466586.html b/image/test/mochitest/test_bug466586.html new file mode 100644 index 0000000000..fb900dc2b3 --- /dev/null +++ b/image/test/mochitest/test_bug466586.html @@ -0,0 +1,58 @@ + + + + + Test for Bug 466586 + + + + + +Mozilla Bug 466586 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug468160.html b/image/test/mochitest/test_bug468160.html new file mode 100644 index 0000000000..cb33454e1b --- /dev/null +++ b/image/test/mochitest/test_bug468160.html @@ -0,0 +1,29 @@ + + + + + Test for Bug 468160 + + + + +Mozilla Bug 468160 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug478398.html b/image/test/mochitest/test_bug478398.html new file mode 100644 index 0000000000..45ab6acc2f --- /dev/null +++ b/image/test/mochitest/test_bug478398.html @@ -0,0 +1,87 @@ + + + + + Test for Bug 478398 + + + + + +Mozilla Bug 478398 +
    +
    +
    + + + diff --git a/image/test/mochitest/test_bug490949.html b/image/test/mochitest/test_bug490949.html new file mode 100644 index 0000000000..8f1b70c8f4 --- /dev/null +++ b/image/test/mochitest/test_bug490949.html @@ -0,0 +1,112 @@ + + + + + Test for Bug 490949 + + + + + +Mozilla Bug 490949 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug496292.html b/image/test/mochitest/test_bug496292.html new file mode 100644 index 0000000000..7f91539bf3 --- /dev/null +++ b/image/test/mochitest/test_bug496292.html @@ -0,0 +1,130 @@ + + + + + Test for Bug 496292 + + + + + +Mozilla Bug 496292 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug497665.html b/image/test/mochitest/test_bug497665.html new file mode 100644 index 0000000000..3914e43c64 --- /dev/null +++ b/image/test/mochitest/test_bug497665.html @@ -0,0 +1,88 @@ + + + + + Test for Bug 497665 + + + + + +Mozilla Bug 497665 +

    +
    +
    +
    +
    + + +
    + + diff --git a/image/test/mochitest/test_bug552605-1.html b/image/test/mochitest/test_bug552605-1.html new file mode 100644 index 0000000000..271b3599ce --- /dev/null +++ b/image/test/mochitest/test_bug552605-1.html @@ -0,0 +1,56 @@ + + + + + Test for Bug 552605 + + + + + + +Mozilla Bug 552605 +

    +
    +
    +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bug552605-2.html b/image/test/mochitest/test_bug552605-2.html new file mode 100644 index 0000000000..7869a9e481 --- /dev/null +++ b/image/test/mochitest/test_bug552605-2.html @@ -0,0 +1,53 @@ + + + + + Test for Bug 552605 + + + + + + +Mozilla Bug 552605 +

    +
    +
    +
    +
    + + +
    + + diff --git a/image/test/mochitest/test_bug553982.html b/image/test/mochitest/test_bug553982.html new file mode 100644 index 0000000000..f7fe58a0af --- /dev/null +++ b/image/test/mochitest/test_bug553982.html @@ -0,0 +1,39 @@ + + + + + Test for Bug 553982 + + + + + +Mozilla Bug 553982 +
    +
    +
    + + + diff --git a/image/test/mochitest/test_bug601470.html b/image/test/mochitest/test_bug601470.html new file mode 100644 index 0000000000..fdf2d074f8 --- /dev/null +++ b/image/test/mochitest/test_bug601470.html @@ -0,0 +1,45 @@ + + + + + Test for Bug 601470 + + + + +Mozilla Bug 601470 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug614392.html b/image/test/mochitest/test_bug614392.html new file mode 100644 index 0000000000..94585ba35e --- /dev/null +++ b/image/test/mochitest/test_bug614392.html @@ -0,0 +1,42 @@ + + + + + Test for Bug 614392 + + + + +Mozilla Bug 614392 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug657191.html b/image/test/mochitest/test_bug657191.html new file mode 100644 index 0000000000..17fcae6c8a --- /dev/null +++ b/image/test/mochitest/test_bug657191.html @@ -0,0 +1,34 @@ + + + + + Test for Bug 657191 + + + + + +Mozilla Bug 657191 +

    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_bug671906.html b/image/test/mochitest/test_bug671906.html new file mode 100644 index 0000000000..82f70f19d6 --- /dev/null +++ b/image/test/mochitest/test_bug671906.html @@ -0,0 +1,71 @@ + + + + + Test for Bug 671906 + + + + + + +Mozilla Bug 671906 +

    +
    +
    +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bug733553.html b/image/test/mochitest/test_bug733553.html new file mode 100644 index 0000000000..6d7ed81019 --- /dev/null +++ b/image/test/mochitest/test_bug733553.html @@ -0,0 +1,92 @@ + + + + + Test for Bug 733553 + + + + + +Mozilla Bug 733553 +

    +
    +
    +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bug767779.html b/image/test/mochitest/test_bug767779.html new file mode 100644 index 0000000000..ae3fe492cf --- /dev/null +++ b/image/test/mochitest/test_bug767779.html @@ -0,0 +1,44 @@ + + + + + Test for Bug 767779 + + + + + + + + +Mozilla Bug 767779 +

    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bug865919.html b/image/test/mochitest/test_bug865919.html new file mode 100644 index 0000000000..46686c67d7 --- /dev/null +++ b/image/test/mochitest/test_bug865919.html @@ -0,0 +1,53 @@ + + + + + + Test for Bug 865919 + + + + + + + +
    + +
    + + + + diff --git a/image/test/mochitest/test_bug89419-1.html b/image/test/mochitest/test_bug89419-1.html new file mode 100644 index 0000000000..3364dacbfb --- /dev/null +++ b/image/test/mochitest/test_bug89419-1.html @@ -0,0 +1,68 @@ + + + + + Test for Bug 89419 + + + + + + +Mozilla Bug 89419 +

    +
    +
    +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bug89419-2.html b/image/test/mochitest/test_bug89419-2.html new file mode 100644 index 0000000000..25861b9179 --- /dev/null +++ b/image/test/mochitest/test_bug89419-2.html @@ -0,0 +1,70 @@ + + + + + Test for Bug 89419 + + + + + + +Mozilla Bug 89419 +

    +
    +
    +
    +
    + +
    + + diff --git a/image/test/mochitest/test_bullet_animation.html b/image/test/mochitest/test_bullet_animation.html new file mode 100644 index 0000000000..7e39898a4f --- /dev/null +++ b/image/test/mochitest/test_bullet_animation.html @@ -0,0 +1,59 @@ + + + + + Test for Bug 666446 - Animated Bullets + + + + + + + + + +Mozilla Bug 666446: lots of animated gifs swamp us with paint events + +

    + +
    + + + +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_canvas_frame_animation.html b/image/test/mochitest/test_canvas_frame_animation.html new file mode 100644 index 0000000000..c2820b3a66 --- /dev/null +++ b/image/test/mochitest/test_canvas_frame_animation.html @@ -0,0 +1,24 @@ + +Test for bug 1619245 - animated image as canvas background + + + + + + + + + + + diff --git a/image/test/mochitest/test_changeOfSource.html b/image/test/mochitest/test_changeOfSource.html new file mode 100644 index 0000000000..f557a3a140 --- /dev/null +++ b/image/test/mochitest/test_changeOfSource.html @@ -0,0 +1,62 @@ + + + + + Test for Bug 666446 - Change of Source (1st Version) + + + + + + + + +Mozilla Bug 666446: lots of animated gifs swamp us with paint events + +

    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_changeOfSource2.html b/image/test/mochitest/test_changeOfSource2.html new file mode 100644 index 0000000000..e3db345470 --- /dev/null +++ b/image/test/mochitest/test_changeOfSource2.html @@ -0,0 +1,47 @@ + + + + + Test for Bug 691792 - Change of Source (2nd Version) + + + + + + + + +Mozilla Bug 691792: Change of src attribute for animated gifs no longer works as expected + +

    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_discardAnimatedImage.html b/image/test/mochitest/test_discardAnimatedImage.html new file mode 100644 index 0000000000..d2aad380e0 --- /dev/null +++ b/image/test/mochitest/test_discardAnimatedImage.html @@ -0,0 +1,218 @@ + + + + + Test that animated images can be discarded + + + + + + +Mozilla Bug 686905 +

    +
    +
    + + + + + + + + + +
    +
    +
    +
    +
    + + + diff --git a/image/test/mochitest/test_discardFinishedAnimatedImage.html b/image/test/mochitest/test_discardFinishedAnimatedImage.html new file mode 100644 index 0000000000..190cb1d1a0 --- /dev/null +++ b/image/test/mochitest/test_discardFinishedAnimatedImage.html @@ -0,0 +1,144 @@ + + + + Test that img.decode works on finished, discarded animated images + + + + + + +Mozilla Bug 1629490 +
    + +
    + + + diff --git a/image/test/mochitest/test_discardFramesAnimatedImage.html b/image/test/mochitest/test_discardFramesAnimatedImage.html new file mode 100644 index 0000000000..2e95e6203b --- /dev/null +++ b/image/test/mochitest/test_discardFramesAnimatedImage.html @@ -0,0 +1,268 @@ + + + + + Test that animated images can discard frames and redecode + + + + + + +Mozilla Bug 523950 +

    +
    +
    + + +
    +
    +
    +
    +
    + + + diff --git a/image/test/mochitest/test_drawDiscardedImage.html b/image/test/mochitest/test_drawDiscardedImage.html new file mode 100644 index 0000000000..f1a2dde89c --- /dev/null +++ b/image/test/mochitest/test_drawDiscardedImage.html @@ -0,0 +1,85 @@ + + + + + Test for Bug 731419 - Draw an ostensibly discarded image to a canvas + + + + + + + + + + + + + + + + + diff --git a/image/test/mochitest/test_error_events.html b/image/test/mochitest/test_error_events.html new file mode 100644 index 0000000000..89ed9da528 --- /dev/null +++ b/image/test/mochitest/test_error_events.html @@ -0,0 +1,67 @@ + + + + + Test for Bug 715308 comment 93 + + + + + + + + + +
    + + +
    + + + + + diff --git a/image/test/mochitest/test_has_transparency.html b/image/test/mochitest/test_has_transparency.html new file mode 100644 index 0000000000..482aaf96b9 --- /dev/null +++ b/image/test/mochitest/test_has_transparency.html @@ -0,0 +1,169 @@ + + + + + Test for Bug 1089880 + + + + + + +Mozilla Bug 1089880 +

    +
    +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_image_cache_notification.html b/image/test/mochitest/test_image_cache_notification.html new file mode 100644 index 0000000000..73adac25ff --- /dev/null +++ b/image/test/mochitest/test_image_cache_notification.html @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/image/test/mochitest/test_image_crossorigin_data_url.html b/image/test/mochitest/test_image_crossorigin_data_url.html new file mode 100644 index 0000000000..4465b5aab4 --- /dev/null +++ b/image/test/mochitest/test_image_crossorigin_data_url.html @@ -0,0 +1,28 @@ + + +Test for handling of 'crossorigin' attribute on CSS link with data: URL + + +
    +
    + + diff --git a/image/test/mochitest/test_mq_dynamic_svg.html b/image/test/mochitest/test_mq_dynamic_svg.html new file mode 100644 index 0000000000..25b708224e --- /dev/null +++ b/image/test/mochitest/test_mq_dynamic_svg.html @@ -0,0 +1,49 @@ + + +Dynamic changes to prefers-color-scheme affecting SVG images + + + + + + + diff --git a/image/test/mochitest/test_net_failedtoprocess.html b/image/test/mochitest/test_net_failedtoprocess.html new file mode 100644 index 0000000000..470f3f5b31 --- /dev/null +++ b/image/test/mochitest/test_net_failedtoprocess.html @@ -0,0 +1,52 @@ + + + + + Test for image net:failed-to-process-uri-content + + + + +

    +
    +
    + + + diff --git a/image/test/mochitest/test_removal_ondecode.html b/image/test/mochitest/test_removal_ondecode.html new file mode 100644 index 0000000000..4ce7555757 --- /dev/null +++ b/image/test/mochitest/test_removal_ondecode.html @@ -0,0 +1,160 @@ + + + + + Test for Bug 841579 + + + + + + +Mozilla Bug 841579 +

    +
    +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_removal_onload.html b/image/test/mochitest/test_removal_onload.html new file mode 100644 index 0000000000..0a060542f5 --- /dev/null +++ b/image/test/mochitest/test_removal_onload.html @@ -0,0 +1,142 @@ + + + + + Test for Bug 841579 + + + + + + +Mozilla Bug 841579 +

    +
    +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_short_gif_header.html b/image/test/mochitest/test_short_gif_header.html new file mode 100644 index 0000000000..f8294825b5 --- /dev/null +++ b/image/test/mochitest/test_short_gif_header.html @@ -0,0 +1,35 @@ + + + + + Test for Bug 844684 + + + + + +Mozilla Bug 844684 +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_staticClone.html b/image/test/mochitest/test_staticClone.html new file mode 100644 index 0000000000..7400e064c6 --- /dev/null +++ b/image/test/mochitest/test_staticClone.html @@ -0,0 +1,41 @@ + + + + + Test for Bug 878037 + + + + +Mozilla Bug 878037 +

    +
    + + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_svg_animatedGIF.html b/image/test/mochitest/test_svg_animatedGIF.html new file mode 100644 index 0000000000..04077676b7 --- /dev/null +++ b/image/test/mochitest/test_svg_animatedGIF.html @@ -0,0 +1,53 @@ + + + + + Test for Bug 666446 - Animated Raster Images inside of SVG Frames + + + + + + + + + + +

    +
    +
    + + +
    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_svg_filter_animation.html b/image/test/mochitest/test_svg_filter_animation.html new file mode 100644 index 0000000000..b72aaa68c1 --- /dev/null +++ b/image/test/mochitest/test_svg_filter_animation.html @@ -0,0 +1,42 @@ + + + + + Test for Bug 666446 - Animated Images within SVG Filters + + + + + + + + +Mozilla Bug 666446: lots of animated gifs swamp us with paint events + +

    +
    + + +
    + +
    +
    +
    + + diff --git a/image/test/mochitest/test_synchronized_animation.html b/image/test/mochitest/test_synchronized_animation.html new file mode 100644 index 0000000000..3c2a874be1 --- /dev/null +++ b/image/test/mochitest/test_synchronized_animation.html @@ -0,0 +1,128 @@ + + + + + Test for Bug 867758 + + + + + + +Mozilla Bug 867758 +

    +
    +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_undisplayed_iframe.html b/image/test/mochitest/test_undisplayed_iframe.html new file mode 100644 index 0000000000..695e4e5448 --- /dev/null +++ b/image/test/mochitest/test_undisplayed_iframe.html @@ -0,0 +1,47 @@ + + + + +Test for Bug 666446 - Test for Animated Gif within IFRAME + + + + + + + + + Mozilla Bug 666446: lots of animated gifs swamp us with paint events +

    + +
    + +
    + +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_webcam.html b/image/test/mochitest/test_webcam.html new file mode 100644 index 0000000000..8d6bf6b490 --- /dev/null +++ b/image/test/mochitest/test_webcam.html @@ -0,0 +1,68 @@ + + + + + Test for Bug 641748 - WebCam Simulacrum + + + + + + + + +Mozilla Bug 641748: GIF decoder doesn't support multipart/x-mixed-replace + +

    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/image/test/mochitest/test_xultree_animation.xhtml b/image/test/mochitest/test_xultree_animation.xhtml new file mode 100644 index 0000000000..801117ed37 --- /dev/null +++ b/image/test/mochitest/test_xultree_animation.xhtml @@ -0,0 +1,67 @@ + + + + + Test for Bug 666446 - Animated Images within SVG Filters + + + + + + + + +Mozilla Bug 666446: lots of animated gifs swamp us with paint events + +

    +
    + + +
    + + + + + + + + + + + + +
    + +
    +
    +
    + + diff --git a/image/test/mochitest/transparent.gif b/image/test/mochitest/transparent.gif new file mode 100644 index 0000000000..48f5c7caf1 Binary files /dev/null and b/image/test/mochitest/transparent.gif differ diff --git a/image/test/mochitest/transparent.png b/image/test/mochitest/transparent.png new file mode 100644 index 0000000000..fc8002053a Binary files /dev/null and b/image/test/mochitest/transparent.png differ diff --git a/image/test/mochitest/webcam-simulacrum.sjs b/image/test/mochitest/webcam-simulacrum.sjs new file mode 100644 index 0000000000..6243329eac --- /dev/null +++ b/image/test/mochitest/webcam-simulacrum.sjs @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var counter = 2; +var frames = ["red.gif", "blue.gif"]; +var timer = Cc["@mozilla.org/timer;1"]; +var partTimer = timer.createInstance(Ci.nsITimer); + +function getFileAsInputStream(aFilename) { + var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + file.append("tests"); + file.append("image"); + file.append("test"); + file.append("mochitest"); + file.append(aFilename); + + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function handleRequest(request, response) { + response.setHeader( + "Content-Type", + "multipart/x-mixed-replace;boundary=BOUNDARYOMG", + false + ); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.processAsync(); + response.write("--BOUNDARYOMG\r\n"); + while (frames.length) { + sendNextPart(response); + } + response.write("--BOUNDARYOMG--\r\n"); + response.finish(); +} + +function sendNextPart(response) { + var nextPartHead = "Content-Type: image/gif\r\n\r\n"; + var inputStream = getFileAsInputStream(frames.shift()); + response.bodyOutputStream.write(nextPartHead, nextPartHead.length); + response.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + // Toss in the boundary, so the browser can know this part is complete + response.write("--BOUNDARYOMG\r\n"); +} diff --git a/image/test/reftest/ImageDocument.css b/image/test/reftest/ImageDocument.css new file mode 100644 index 0000000000..b449810986 --- /dev/null +++ b/image/test/reftest/ImageDocument.css @@ -0,0 +1,16 @@ +body { + background-image: url("chrome://global/skin/media/imagedoc-darknoise.png"); + margin: 0; +} + +body > :first-child { + display: block; + position: absolute; + margin: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png"); + color: #222; +} diff --git a/image/test/reftest/apng/bug411852-1-ref.html b/image/test/reftest/apng/bug411852-1-ref.html new file mode 100644 index 0000000000..ce346c87ca --- /dev/null +++ b/image/test/reftest/apng/bug411852-1-ref.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/image/test/reftest/apng/bug411852-1-ref.png b/image/test/reftest/apng/bug411852-1-ref.png new file mode 100644 index 0000000000..04b862a1d9 Binary files /dev/null and b/image/test/reftest/apng/bug411852-1-ref.png differ diff --git a/image/test/reftest/apng/bug411852-1.png b/image/test/reftest/apng/bug411852-1.png new file mode 100644 index 0000000000..643f87e176 Binary files /dev/null and b/image/test/reftest/apng/bug411852-1.png differ diff --git a/image/test/reftest/apng/bug546272-ref.html b/image/test/reftest/apng/bug546272-ref.html new file mode 100644 index 0000000000..43afef15e1 --- /dev/null +++ b/image/test/reftest/apng/bug546272-ref.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/image/test/reftest/apng/bug546272-ref.png b/image/test/reftest/apng/bug546272-ref.png new file mode 100644 index 0000000000..85dfd8ccfd Binary files /dev/null and b/image/test/reftest/apng/bug546272-ref.png differ diff --git a/image/test/reftest/apng/bug546272.png b/image/test/reftest/apng/bug546272.png new file mode 100644 index 0000000000..5232d7f8f2 Binary files /dev/null and b/image/test/reftest/apng/bug546272.png differ diff --git a/image/test/reftest/apng/delaytest.html b/image/test/reftest/apng/delaytest.html new file mode 100644 index 0000000000..ee79254a2b --- /dev/null +++ b/image/test/reftest/apng/delaytest.html @@ -0,0 +1,58 @@ + + + +Delayed image reftest wrapper + + + + + + + diff --git a/image/test/reftest/apng/reftest.list b/image/test/reftest/apng/reftest.list new file mode 100644 index 0000000000..7ff6e561fe --- /dev/null +++ b/image/test/reftest/apng/reftest.list @@ -0,0 +1,6 @@ +# APNG tests +# +# delaytest.html delays the reftest snapshot to allow time for the +# animation to complete. +== delaytest.html?bug411852-1.png bug411852-1-ref.html +== delaytest.html?bug546272.png bug546272-ref.html diff --git a/image/test/reftest/avif/1-normal.avif b/image/test/reftest/avif/1-normal.avif new file mode 100644 index 0000000000..5cc9b295da Binary files /dev/null and b/image/test/reftest/avif/1-normal.avif differ diff --git a/image/test/reftest/avif/2-flipped-horizontally.avif b/image/test/reftest/avif/2-flipped-horizontally.avif new file mode 100644 index 0000000000..f79492f776 Binary files /dev/null and b/image/test/reftest/avif/2-flipped-horizontally.avif differ diff --git a/image/test/reftest/avif/3-rotated-180deg.avif b/image/test/reftest/avif/3-rotated-180deg.avif new file mode 100644 index 0000000000..ba8f4565fe Binary files /dev/null and b/image/test/reftest/avif/3-rotated-180deg.avif differ diff --git a/image/test/reftest/avif/4-flipped-vertically.avif b/image/test/reftest/avif/4-flipped-vertically.avif new file mode 100644 index 0000000000..77c2e651b8 Binary files /dev/null and b/image/test/reftest/avif/4-flipped-vertically.avif differ diff --git a/image/test/reftest/avif/5-rotated-90deg-CCW-and-flipped-vertically.avif b/image/test/reftest/avif/5-rotated-90deg-CCW-and-flipped-vertically.avif new file mode 100644 index 0000000000..4bfa631159 Binary files /dev/null and b/image/test/reftest/avif/5-rotated-90deg-CCW-and-flipped-vertically.avif differ diff --git a/image/test/reftest/avif/6-rotated-90deg-CCW.avif b/image/test/reftest/avif/6-rotated-90deg-CCW.avif new file mode 100644 index 0000000000..558356532e Binary files /dev/null and b/image/test/reftest/avif/6-rotated-90deg-CCW.avif differ diff --git a/image/test/reftest/avif/7-rotated-90deg-CW-and-flipped-vertically.avif b/image/test/reftest/avif/7-rotated-90deg-CW-and-flipped-vertically.avif new file mode 100644 index 0000000000..08495f620b Binary files /dev/null and b/image/test/reftest/avif/7-rotated-90deg-CW-and-flipped-vertically.avif differ diff --git a/image/test/reftest/avif/8-rotated-90deg-CW.avif b/image/test/reftest/avif/8-rotated-90deg-CW.avif new file mode 100644 index 0000000000..112bdac39e Binary files /dev/null and b/image/test/reftest/avif/8-rotated-90deg-CW.avif differ diff --git a/image/test/reftest/avif/img_irot0_imir0.avif b/image/test/reftest/avif/img_irot0_imir0.avif new file mode 100644 index 0000000000..95ddcf0003 Binary files /dev/null and b/image/test/reftest/avif/img_irot0_imir0.avif differ diff --git a/image/test/reftest/avif/img_irot0_imir1.avif b/image/test/reftest/avif/img_irot0_imir1.avif new file mode 100644 index 0000000000..7006cbd98a Binary files /dev/null and b/image/test/reftest/avif/img_irot0_imir1.avif differ diff --git a/image/test/reftest/avif/img_irot0_imirN.avif b/image/test/reftest/avif/img_irot0_imirN.avif new file mode 100644 index 0000000000..1455f67cd8 Binary files /dev/null and b/image/test/reftest/avif/img_irot0_imirN.avif differ diff --git a/image/test/reftest/avif/img_irot1_imir0.avif b/image/test/reftest/avif/img_irot1_imir0.avif new file mode 100644 index 0000000000..4069846f72 Binary files /dev/null and b/image/test/reftest/avif/img_irot1_imir0.avif differ diff --git a/image/test/reftest/avif/img_irot1_imir1.avif b/image/test/reftest/avif/img_irot1_imir1.avif new file mode 100644 index 0000000000..60f8b63739 Binary files /dev/null and b/image/test/reftest/avif/img_irot1_imir1.avif differ diff --git a/image/test/reftest/avif/img_irot1_imirN.avif b/image/test/reftest/avif/img_irot1_imirN.avif new file mode 100644 index 0000000000..141e46c5ae Binary files /dev/null and b/image/test/reftest/avif/img_irot1_imirN.avif differ diff --git a/image/test/reftest/avif/img_irot2_imir0.avif b/image/test/reftest/avif/img_irot2_imir0.avif new file mode 100644 index 0000000000..d241c6f3c0 Binary files /dev/null and b/image/test/reftest/avif/img_irot2_imir0.avif differ diff --git a/image/test/reftest/avif/img_irot2_imir1.avif b/image/test/reftest/avif/img_irot2_imir1.avif new file mode 100644 index 0000000000..4af2f57522 Binary files /dev/null and b/image/test/reftest/avif/img_irot2_imir1.avif differ diff --git a/image/test/reftest/avif/img_irot2_imirN.avif b/image/test/reftest/avif/img_irot2_imirN.avif new file mode 100644 index 0000000000..fc3595b672 Binary files /dev/null and b/image/test/reftest/avif/img_irot2_imirN.avif differ diff --git a/image/test/reftest/avif/img_irot3_imir0.avif b/image/test/reftest/avif/img_irot3_imir0.avif new file mode 100644 index 0000000000..f8c7c6b7a1 Binary files /dev/null and b/image/test/reftest/avif/img_irot3_imir0.avif differ diff --git a/image/test/reftest/avif/img_irot3_imir1.avif b/image/test/reftest/avif/img_irot3_imir1.avif new file mode 100644 index 0000000000..971587853b Binary files /dev/null and b/image/test/reftest/avif/img_irot3_imir1.avif differ diff --git a/image/test/reftest/avif/img_irot3_imirN.avif b/image/test/reftest/avif/img_irot3_imirN.avif new file mode 100644 index 0000000000..8235a2b0c2 Binary files /dev/null and b/image/test/reftest/avif/img_irot3_imirN.avif differ diff --git a/image/test/reftest/avif/img_irotN_imir0.avif b/image/test/reftest/avif/img_irotN_imir0.avif new file mode 100644 index 0000000000..163690ff71 Binary files /dev/null and b/image/test/reftest/avif/img_irotN_imir0.avif differ diff --git a/image/test/reftest/avif/img_irotN_imir1.avif b/image/test/reftest/avif/img_irotN_imir1.avif new file mode 100644 index 0000000000..9642b22411 Binary files /dev/null and b/image/test/reftest/avif/img_irotN_imir1.avif differ diff --git a/image/test/reftest/avif/img_irotN_imirN.avif b/image/test/reftest/avif/img_irotN_imirN.avif new file mode 100644 index 0000000000..5cc9b295da Binary files /dev/null and b/image/test/reftest/avif/img_irotN_imirN.avif differ diff --git a/image/test/reftest/avif/reftest.list b/image/test/reftest/avif/reftest.list new file mode 100644 index 0000000000..10dc2f2d82 --- /dev/null +++ b/image/test/reftest/avif/reftest.list @@ -0,0 +1,17 @@ +defaults pref(image.avif.enabled,true) pref(image.avif.apply_transforms,true) + +== img_irot0_imir1.avif 2-flipped-horizontally.avif +== img_irot0_imir0.avif 4-flipped-vertically.avif +== img_irot0_imirN.avif 1-normal.avif +== img_irot1_imir1.avif 7-rotated-90deg-CW-and-flipped-vertically.avif +== img_irot1_imir0.avif 5-rotated-90deg-CCW-and-flipped-vertically.avif +== img_irot1_imirN.avif 6-rotated-90deg-CCW.avif +== img_irot2_imir1.avif 4-flipped-vertically.avif +== img_irot2_imir0.avif 2-flipped-horizontally.avif +== img_irot2_imirN.avif 3-rotated-180deg.avif +== img_irot3_imir1.avif 5-rotated-90deg-CCW-and-flipped-vertically.avif +== img_irot3_imir0.avif 7-rotated-90deg-CW-and-flipped-vertically.avif +== img_irot3_imirN.avif 8-rotated-90deg-CW.avif +== img_irotN_imir1.avif 2-flipped-horizontally.avif +== img_irotN_imir0.avif 4-flipped-vertically.avif +== img_irotN_imirN.avif 1-normal.avif diff --git a/image/test/reftest/bmp/1240629-1.bmp b/image/test/reftest/bmp/1240629-1.bmp new file mode 100644 index 0000000000..604d248e74 Binary files /dev/null and b/image/test/reftest/bmp/1240629-1.bmp differ diff --git a/image/test/reftest/bmp/1240629-2.bmp b/image/test/reftest/bmp/1240629-2.bmp new file mode 100644 index 0000000000..e4fe80d595 Binary files /dev/null and b/image/test/reftest/bmp/1240629-2.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.bmp new file mode 100644 index 0000000000..302e0c7120 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.png new file mode 100644 index 0000000000..f9318693db Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-not-square-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.bmp new file mode 100644 index 0000000000..e769ff864c Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.png new file mode 100644 index 0000000000..956c78ece6 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-15x15-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.bmp new file mode 100644 index 0000000000..ff012d98c5 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.png new file mode 100644 index 0000000000..90088351fa Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-16x16-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.bmp new file mode 100644 index 0000000000..86f56476e9 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.png new file mode 100644 index 0000000000..9a294696c4 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-17x17-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.bmp new file mode 100644 index 0000000000..0f98654d84 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.ico b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.ico new file mode 100644 index 0000000000..5af8bef61a Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.ico differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.png new file mode 100644 index 0000000000..7a07a124ea Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-1x1-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.bmp new file mode 100644 index 0000000000..5544c6437a Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.png new file mode 100644 index 0000000000..3b09f80769 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-2x2-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.bmp new file mode 100644 index 0000000000..8afcc56cc4 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.png new file mode 100644 index 0000000000..d1fe6ddeee Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-31x31-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.bmp new file mode 100644 index 0000000000..255e5526c7 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.png new file mode 100644 index 0000000000..078d3dc5d0 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-32x32-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.bmp new file mode 100644 index 0000000000..6d752a2e1d Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.png new file mode 100644 index 0000000000..e64e12b2aa Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-33x33-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.bmp new file mode 100644 index 0000000000..d4f885687d Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.png new file mode 100644 index 0000000000..b8519a8749 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-3x3-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.bmp new file mode 100644 index 0000000000..c0b9128feb Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.png new file mode 100644 index 0000000000..3977b54548 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-4x4-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.bmp new file mode 100644 index 0000000000..c02b2288d7 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.png new file mode 100644 index 0000000000..caa9246b67 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-5x5-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.bmp new file mode 100644 index 0000000000..64415c6ecb Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.png new file mode 100644 index 0000000000..30e1b0249d Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-6x6-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.bmp new file mode 100644 index 0000000000..d8e867a0d8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.png new file mode 100644 index 0000000000..9dbaae84cb Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-7x7-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.bmp new file mode 100644 index 0000000000..207e84f80e Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.png new file mode 100644 index 0000000000..2201388408 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-8x8-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.bmp new file mode 100644 index 0000000000..871eb7c0f3 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.png b/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.png new file mode 100644 index 0000000000..7fe1b548b8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/bmp-size-9x9-1bpp.png differ diff --git a/image/test/reftest/bmp/bmp-1bpp/os2bmp-size-32x32-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/os2bmp-size-32x32-1bpp.bmp new file mode 100644 index 0000000000..32bfc5e8f9 Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/os2bmp-size-32x32-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-1bpp/reftest.list b/image/test/reftest/bmp/bmp-1bpp/reftest.list new file mode 100644 index 0000000000..15274c2bc8 --- /dev/null +++ b/image/test/reftest/bmp/bmp-1bpp/reftest.list @@ -0,0 +1,21 @@ +# BMP 1BPP tests + +# Images of various sizes +== bmp-size-1x1-1bpp.bmp bmp-size-1x1-1bpp.png +== bmp-size-2x2-1bpp.bmp bmp-size-2x2-1bpp.png +== bmp-size-3x3-1bpp.bmp bmp-size-3x3-1bpp.png +== bmp-size-4x4-1bpp.bmp bmp-size-4x4-1bpp.png +== bmp-size-5x5-1bpp.bmp bmp-size-5x5-1bpp.png +== bmp-size-6x6-1bpp.bmp bmp-size-6x6-1bpp.png +== bmp-size-7x7-1bpp.bmp bmp-size-7x7-1bpp.png +== bmp-size-8x8-1bpp.bmp bmp-size-8x8-1bpp.png +== bmp-size-9x9-1bpp.bmp bmp-size-9x9-1bpp.png +== bmp-size-15x15-1bpp.bmp bmp-size-15x15-1bpp.png +== bmp-size-16x16-1bpp.bmp bmp-size-16x16-1bpp.png +== bmp-size-17x17-1bpp.bmp bmp-size-17x17-1bpp.png +== bmp-size-31x31-1bpp.bmp bmp-size-31x31-1bpp.png +== bmp-size-32x32-1bpp.bmp bmp-size-32x32-1bpp.png +== bmp-size-33x33-1bpp.bmp bmp-size-33x33-1bpp.png +== bmp-not-square-1bpp.bmp bmp-not-square-1bpp.png +== os2bmp-size-32x32-1bpp.bmp bmp-size-32x32-1bpp.png +== top-to-bottom-16x16-1bpp.bmp bmp-size-16x16-1bpp.png diff --git a/image/test/reftest/bmp/bmp-1bpp/top-to-bottom-16x16-1bpp.bmp b/image/test/reftest/bmp/bmp-1bpp/top-to-bottom-16x16-1bpp.bmp new file mode 100644 index 0000000000..8633ef2aaa Binary files /dev/null and b/image/test/reftest/bmp/bmp-1bpp/top-to-bottom-16x16-1bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.bmp new file mode 100644 index 0000000000..9d1f4de2cd Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.png new file mode 100644 index 0000000000..9b0d160815 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-not-square-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.bmp new file mode 100644 index 0000000000..ba029510b3 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-15x15-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.bmp new file mode 100644 index 0000000000..f35d706692 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-16x16-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.bmp new file mode 100644 index 0000000000..fc576c4980 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-17x17-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.bmp new file mode 100644 index 0000000000..db790d50c0 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-1x1-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.bmp new file mode 100644 index 0000000000..19bff3d010 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-2x2-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.bmp new file mode 100644 index 0000000000..da11048cb0 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-31x31-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.bmp new file mode 100644 index 0000000000..e1631e5fdb Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-32x32-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.bmp new file mode 100644 index 0000000000..d228cf0635 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-33x33-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.bmp new file mode 100644 index 0000000000..f353f9b6d3 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-3x3-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.bmp new file mode 100644 index 0000000000..2373435f48 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-4x4-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.bmp new file mode 100644 index 0000000000..a3016fc1ab Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-5x5-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.bmp new file mode 100644 index 0000000000..cba1d62cc7 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-6x6-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.bmp new file mode 100644 index 0000000000..87cd419b40 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-7x7-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.bmp new file mode 100644 index 0000000000..b6f108a044 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-8x8-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.bmp new file mode 100644 index 0000000000..8140b1905b Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.png b/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/bmp-size-9x9-24bpp.png differ diff --git a/image/test/reftest/bmp/bmp-24bpp/os2bmp-size-32x32-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/os2bmp-size-32x32-24bpp.bmp new file mode 100644 index 0000000000..b75ae62ca8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/os2bmp-size-32x32-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-24bpp/reftest.list b/image/test/reftest/bmp/bmp-24bpp/reftest.list new file mode 100644 index 0000000000..83ec17563b --- /dev/null +++ b/image/test/reftest/bmp/bmp-24bpp/reftest.list @@ -0,0 +1,21 @@ +# BMP 24BPP tests + +# Images of various sizes +== bmp-size-1x1-24bpp.bmp bmp-size-1x1-24bpp.png +== bmp-size-2x2-24bpp.bmp bmp-size-2x2-24bpp.png +== bmp-size-3x3-24bpp.bmp bmp-size-3x3-24bpp.png +== bmp-size-4x4-24bpp.bmp bmp-size-4x4-24bpp.png +== bmp-size-5x5-24bpp.bmp bmp-size-5x5-24bpp.png +== bmp-size-6x6-24bpp.bmp bmp-size-6x6-24bpp.png +== bmp-size-7x7-24bpp.bmp bmp-size-7x7-24bpp.png +== bmp-size-8x8-24bpp.bmp bmp-size-8x8-24bpp.png +== bmp-size-9x9-24bpp.bmp bmp-size-9x9-24bpp.png +== bmp-size-15x15-24bpp.bmp bmp-size-15x15-24bpp.png +== bmp-size-16x16-24bpp.bmp bmp-size-16x16-24bpp.png +== bmp-size-17x17-24bpp.bmp bmp-size-17x17-24bpp.png +== bmp-size-31x31-24bpp.bmp bmp-size-31x31-24bpp.png +== bmp-size-32x32-24bpp.bmp bmp-size-32x32-24bpp.png +== bmp-size-33x33-24bpp.bmp bmp-size-33x33-24bpp.png +== bmp-not-square-24bpp.bmp bmp-not-square-24bpp.png +== os2bmp-size-32x32-24bpp.bmp bmp-size-32x32-24bpp.png +== top-to-bottom-16x16-24bpp.bmp bmp-size-16x16-24bpp.png diff --git a/image/test/reftest/bmp/bmp-24bpp/top-to-bottom-16x16-24bpp.bmp b/image/test/reftest/bmp/bmp-24bpp/top-to-bottom-16x16-24bpp.bmp new file mode 100644 index 0000000000..bd18f85d48 Binary files /dev/null and b/image/test/reftest/bmp/bmp-24bpp/top-to-bottom-16x16-24bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.bmp new file mode 100644 index 0000000000..f63dd81bde Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.png new file mode 100644 index 0000000000..7c713c557c Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-not-square-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.bmp new file mode 100644 index 0000000000..8b586dbfd6 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.png new file mode 100644 index 0000000000..5d4a3f9534 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-15x15-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.bmp new file mode 100644 index 0000000000..eae432e656 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.png new file mode 100644 index 0000000000..d45d63f539 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-16x16-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.bmp new file mode 100644 index 0000000000..5880c6111c Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.png new file mode 100644 index 0000000000..bf48903299 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-17x17-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.bmp new file mode 100644 index 0000000000..2ba68a3913 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.png new file mode 100644 index 0000000000..d41dd645b7 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-1x1-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.bmp new file mode 100644 index 0000000000..6c6383aa80 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.png new file mode 100644 index 0000000000..b2d6050415 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-2x2-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.bmp new file mode 100644 index 0000000000..ac440a6d8d Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.png new file mode 100644 index 0000000000..cb12a3448d Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-31x31-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.bmp new file mode 100644 index 0000000000..e4383c473a Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.png new file mode 100644 index 0000000000..58d867d120 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-32x32-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.bmp new file mode 100644 index 0000000000..04b2c1d1fd Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.png new file mode 100644 index 0000000000..064fde198c Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-33x33-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.bmp new file mode 100644 index 0000000000..179dbcfa59 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.png new file mode 100644 index 0000000000..e34114d5c9 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-3x3-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.bmp new file mode 100644 index 0000000000..0f57e102e2 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.png new file mode 100644 index 0000000000..3efa555620 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-4x4-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.bmp new file mode 100644 index 0000000000..a4efe66601 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.png new file mode 100644 index 0000000000..02ebf57a51 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-5x5-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.bmp new file mode 100644 index 0000000000..f4e1a29185 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.png new file mode 100644 index 0000000000..1f5769d09c Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-6x6-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.bmp new file mode 100644 index 0000000000..e7ee1cf20a Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.png new file mode 100644 index 0000000000..59a1b98b52 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-7x7-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.bmp new file mode 100644 index 0000000000..aa6959baf5 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.png new file mode 100644 index 0000000000..cf44f59676 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-8x8-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.bmp new file mode 100644 index 0000000000..65ec12a376 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.png b/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.png new file mode 100644 index 0000000000..2e07364135 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/bmp-size-9x9-4bpp.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/os2bmp-size-32x32-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/os2bmp-size-32x32-4bpp.bmp new file mode 100644 index 0000000000..08fc30d5fc Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/os2bmp-size-32x32-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/reftest.list b/image/test/reftest/bmp/bmp-4bpp/reftest.list new file mode 100644 index 0000000000..4a1785e86f --- /dev/null +++ b/image/test/reftest/bmp/bmp-4bpp/reftest.list @@ -0,0 +1,24 @@ +# BMP 4BPP tests + +# Images of various sizes +== bmp-size-1x1-4bpp.bmp bmp-size-1x1-4bpp.png +== bmp-size-2x2-4bpp.bmp bmp-size-2x2-4bpp.png +== bmp-size-3x3-4bpp.bmp bmp-size-3x3-4bpp.png +== bmp-size-4x4-4bpp.bmp bmp-size-4x4-4bpp.png +== bmp-size-5x5-4bpp.bmp bmp-size-5x5-4bpp.png +== bmp-size-6x6-4bpp.bmp bmp-size-6x6-4bpp.png +== bmp-size-7x7-4bpp.bmp bmp-size-7x7-4bpp.png +== bmp-size-8x8-4bpp.bmp bmp-size-8x8-4bpp.png +== bmp-size-9x9-4bpp.bmp bmp-size-9x9-4bpp.png +== bmp-size-15x15-4bpp.bmp bmp-size-15x15-4bpp.png +== bmp-size-16x16-4bpp.bmp bmp-size-16x16-4bpp.png +== bmp-size-17x17-4bpp.bmp bmp-size-17x17-4bpp.png +== bmp-size-31x31-4bpp.bmp bmp-size-31x31-4bpp.png +== bmp-size-32x32-4bpp.bmp bmp-size-32x32-4bpp.png +== bmp-size-33x33-4bpp.bmp bmp-size-33x33-4bpp.png +== bmp-not-square-4bpp.bmp bmp-not-square-4bpp.png +== os2bmp-size-32x32-4bpp.bmp bmp-size-32x32-4bpp.png +== top-to-bottom-16x16-4bpp.bmp bmp-size-16x16-4bpp.png +# test that delta skips are drawn as transparent +# taken from http://bmptestsuite.sourceforge.net/ +== rle4-delta-320x240.bmp rle4-delta-320x240.png diff --git a/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.bmp b/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.bmp new file mode 100644 index 0000000000..78a0927870 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.bmp differ diff --git a/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.png b/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.png new file mode 100644 index 0000000000..f9a3ceae23 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/rle4-delta-320x240.png differ diff --git a/image/test/reftest/bmp/bmp-4bpp/top-to-bottom-16x16-4bpp.bmp b/image/test/reftest/bmp/bmp-4bpp/top-to-bottom-16x16-4bpp.bmp new file mode 100644 index 0000000000..c77696b325 Binary files /dev/null and b/image/test/reftest/bmp/bmp-4bpp/top-to-bottom-16x16-4bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-extrapad-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-extrapad-8bpp.png new file mode 100644 index 0000000000..827de11a52 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-extrapad-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.bmp new file mode 100644 index 0000000000..d7a99164c2 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.png new file mode 100644 index 0000000000..be45f19d50 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-not-square-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.bmp new file mode 100644 index 0000000000..8dac8ec86c Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.png new file mode 100644 index 0000000000..ce00553055 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-15x15-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.bmp new file mode 100644 index 0000000000..bb60249ac8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.png new file mode 100644 index 0000000000..6a2394618f Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-16x16-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.bmp new file mode 100644 index 0000000000..b817888510 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.png new file mode 100644 index 0000000000..494cd96cd6 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-17x17-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.bmp new file mode 100644 index 0000000000..9f3ef5136b Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.png new file mode 100644 index 0000000000..a7553a73f7 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-1x1-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.bmp new file mode 100644 index 0000000000..63d3f1c058 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.png new file mode 100644 index 0000000000..17257e9924 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-2x2-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.bmp new file mode 100644 index 0000000000..e4fd01fe0d Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.png new file mode 100644 index 0000000000..d43ac83900 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-31x31-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.bmp new file mode 100644 index 0000000000..d2f800d673 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.png new file mode 100644 index 0000000000..03642849aa Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-32x32-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.bmp new file mode 100644 index 0000000000..19b0744dbf Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.png new file mode 100644 index 0000000000..078b56df0d Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-33x33-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.bmp new file mode 100644 index 0000000000..9f15f35831 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.png new file mode 100644 index 0000000000..ba34b2601b Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-3x3-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.bmp new file mode 100644 index 0000000000..1ad7a8de11 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.png new file mode 100644 index 0000000000..ecf9e5e795 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-4x4-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.bmp new file mode 100644 index 0000000000..6eb759d061 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.png new file mode 100644 index 0000000000..1a440a16bd Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-5x5-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.bmp new file mode 100644 index 0000000000..a1e0e2415a Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.png new file mode 100644 index 0000000000..e0ac1a8f6d Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-6x6-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.bmp new file mode 100644 index 0000000000..25c59d7350 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.png new file mode 100644 index 0000000000..51c7642656 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-7x7-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.bmp new file mode 100644 index 0000000000..ff5b7681c8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.png new file mode 100644 index 0000000000..77dc7782e4 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-8x8-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.bmp new file mode 100644 index 0000000000..006961628f Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.png b/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.png new file mode 100644 index 0000000000..93914c3e16 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/bmp-size-9x9-8bpp.png differ diff --git a/image/test/reftest/bmp/bmp-8bpp/os2-bmp-size-32x32-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/os2-bmp-size-32x32-8bpp.bmp new file mode 100644 index 0000000000..b6df221e1b Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/os2-bmp-size-32x32-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/reftest.list b/image/test/reftest/bmp/bmp-8bpp/reftest.list new file mode 100644 index 0000000000..6fcef4742a --- /dev/null +++ b/image/test/reftest/bmp/bmp-8bpp/reftest.list @@ -0,0 +1,25 @@ +# BMP 8BPP tests + +# Images of various sizes +== bmp-size-1x1-8bpp.bmp bmp-size-1x1-8bpp.png +== bmp-size-2x2-8bpp.bmp bmp-size-2x2-8bpp.png +== bmp-size-3x3-8bpp.bmp bmp-size-3x3-8bpp.png +== bmp-size-4x4-8bpp.bmp bmp-size-4x4-8bpp.png +== bmp-size-5x5-8bpp.bmp bmp-size-5x5-8bpp.png +== bmp-size-6x6-8bpp.bmp bmp-size-6x6-8bpp.png +== bmp-size-7x7-8bpp.bmp bmp-size-7x7-8bpp.png +== bmp-size-8x8-8bpp.bmp bmp-size-8x8-8bpp.png +== bmp-size-9x9-8bpp.bmp bmp-size-9x9-8bpp.png +== bmp-size-15x15-8bpp.bmp bmp-size-15x15-8bpp.png +== bmp-size-16x16-8bpp.bmp bmp-size-16x16-8bpp.png +== bmp-size-17x17-8bpp.bmp bmp-size-17x17-8bpp.png +== bmp-size-31x31-8bpp.bmp bmp-size-31x31-8bpp.png +== bmp-size-32x32-8bpp.bmp bmp-size-32x32-8bpp.png +== bmp-size-33x33-8bpp.bmp bmp-size-33x33-8bpp.png +== bmp-not-square-8bpp.bmp bmp-not-square-8bpp.png +== rle-bmp-extrapad-8bpp.bmp bmp-extrapad-8bpp.png +== rle-bmp-not-square-8bpp.bmp bmp-not-square-8bpp.png +== os2-bmp-size-32x32-8bpp.bmp bmp-size-32x32-8bpp.png +== rle-bmp-size-32x32-8bpp.bmp bmp-size-32x32-8bpp.png +== top-to-bottom-rle-bmp-size-32x32-8bpp.bmp bmp-size-32x32-8bpp.png +== top-to-bottom-16x16-8bpp.bmp bmp-size-16x16-8bpp.png diff --git a/image/test/reftest/bmp/bmp-8bpp/rle-bmp-extrapad-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-extrapad-8bpp.bmp new file mode 100644 index 0000000000..9550ab2af6 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-extrapad-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/rle-bmp-not-square-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-not-square-8bpp.bmp new file mode 100644 index 0000000000..8687aab6cf Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-not-square-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/rle-bmp-size-32x32-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-size-32x32-8bpp.bmp new file mode 100644 index 0000000000..bd793b6b66 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/rle-bmp-size-32x32-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-16x16-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-16x16-8bpp.bmp new file mode 100644 index 0000000000..bb60249ac8 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-16x16-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-rle-bmp-size-32x32-8bpp.bmp b/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-rle-bmp-size-32x32-8bpp.bmp new file mode 100644 index 0000000000..396672ea15 Binary files /dev/null and b/image/test/reftest/bmp/bmp-8bpp/top-to-bottom-rle-bmp-size-32x32-8bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-bpp.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-bpp.bmp new file mode 100644 index 0000000000..c00dd3fa4c Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-compression-BITFIELDS.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-BITFIELDS.bmp new file mode 100644 index 0000000000..92a7225261 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-BITFIELDS.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE4.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE4.bmp new file mode 100644 index 0000000000..d73c89411d Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE4.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE8.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE8.bmp new file mode 100644 index 0000000000..5a88064543 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-compression-RLE8.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-compression.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-compression.bmp new file mode 100644 index 0000000000..aa3134e562 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-compression.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-data-offset.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-data-offset.bmp new file mode 100644 index 0000000000..b89bc55a8e Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-data-offset.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-signature.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-signature.bmp new file mode 100644 index 0000000000..6eebb57180 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-signature.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/invalid-truncated-metadata.bmp b/image/test/reftest/bmp/bmp-corrupted/invalid-truncated-metadata.bmp new file mode 100644 index 0000000000..228c5c9992 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/invalid-truncated-metadata.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/os2-invalid-bpp.bmp b/image/test/reftest/bmp/bmp-corrupted/os2-invalid-bpp.bmp new file mode 100644 index 0000000000..af4678a284 Binary files /dev/null and b/image/test/reftest/bmp/bmp-corrupted/os2-invalid-bpp.bmp differ diff --git a/image/test/reftest/bmp/bmp-corrupted/reftest.list b/image/test/reftest/bmp/bmp-corrupted/reftest.list new file mode 100644 index 0000000000..882657983a --- /dev/null +++ b/image/test/reftest/bmp/bmp-corrupted/reftest.list @@ -0,0 +1,21 @@ +# Corrupted BMP tests + +== wrapper.html?invalid-signature.bmp about:blank +== wrapper.html?invalid-bpp.bmp about:blank +== wrapper.html?os2-invalid-bpp.bmp about:blank +# Tests for an unsupported compression value +== wrapper.html?invalid-compression.bmp about:blank +# Tests for RLE4 with an invalid BPP +== wrapper.html?invalid-compression-RLE4.bmp about:blank +# Tests for RLE8 with an invalid BPP +== wrapper.html?invalid-compression-RLE8.bmp about:blank + +# Test for BITFIELDS with an invalid BIH size. (This is the obscure +# BITMAPV3INFOHEADER variant mentioned in +# https://en.wikipedia.org/wiki/BMP_file_format which we don't accept.) +== wrapper.html?invalid-compression-BITFIELDS.bmp about:blank + +== wrapper.html?invalid-truncated-metadata.bmp about:blank + +# Test for an invalid data offset +== wrapper.html?invalid-data-offset.bmp about:blank \ No newline at end of file diff --git a/image/test/reftest/bmp/bmp-corrupted/wrapper.html b/image/test/reftest/bmp/bmp-corrupted/wrapper.html new file mode 100644 index 0000000000..22b74c8fc1 --- /dev/null +++ b/image/test/reftest/bmp/bmp-corrupted/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/bmp/bmpsuite/COPYING.txt b/image/test/reftest/bmp/bmpsuite/COPYING.txt new file mode 100644 index 0000000000..10926e87f1 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/COPYING.txt @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/image/test/reftest/bmp/bmpsuite/README.mozilla b/image/test/reftest/bmp/bmpsuite/README.mozilla new file mode 100644 index 0000000000..f59a494c26 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/README.mozilla @@ -0,0 +1,40 @@ +bmpsuite, by Jason Summers, is an excellent BMP test suite that covers many +obscure corners of the BMP format. All the test images can be seen here: + + http://entropymine.com/jason/bmpsuite/bmpsuite/html/bmpsuite.html + +The code used to generate the test images is available here: + + https://github.com/jsummers/bmpsuite/ + +The readme.txt file states that the code is GPLv3 and the generated image files +are in the public domain. We have not included the code, but we have included: +(a) some quotes from the documentation and (b) some of the reference PNG +images. However, (a) and (b) are for testing purposes only and are not included +in Firefox releases. + +The BMP files within this directory were generated with bmpsuite v2.3 (git +revision 3adcc9e20c0b6d2d665966b7e047b6f9474cef12). + +There are three sub-directories. +- g/: for "good" images. +- q/: for "questionable" images. +- b/: for "bad" images. +- x/: for images that arguably may not truly be in "BMP format". + +Each file listed in a reftest.list file is annotated with the following lines. + +- The first line starts with "BMP:" and is the output of the MOZ_LOG call in + nsBMPDecoder.cpp. It has basic image info. + +- Next is a quote from the bmpsuite docs, which describes the particulars of + the file. + +- Some files also have additional notes in square brackets. These explain + anything non-obvious about the file, such as how we handle things that are + ambiguous, any shortcomings in our decoding, and how Chromium handles the + image. + +Some of the reference PNGs need a small amount of fuzziness to match the BMPs. +This might be due to PNG color correction. + diff --git a/image/test/reftest/bmp/bmpsuite/b/badbitcount.bmp b/image/test/reftest/bmp/bmpsuite/b/badbitcount.bmp new file mode 100644 index 0000000000..d4fa4e8b88 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badbitcount.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badbitssize.bmp b/image/test/reftest/bmp/bmpsuite/b/badbitssize.bmp new file mode 100644 index 0000000000..0a99a605af Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badbitssize.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/baddens1.bmp b/image/test/reftest/bmp/bmpsuite/b/baddens1.bmp new file mode 100644 index 0000000000..a6150a6fe7 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/baddens1.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/baddens2.bmp b/image/test/reftest/bmp/bmpsuite/b/baddens2.bmp new file mode 100644 index 0000000000..f2c1dfb665 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/baddens2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badfilesize.bmp b/image/test/reftest/bmp/bmpsuite/b/badfilesize.bmp new file mode 100644 index 0000000000..da52cb51d7 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badfilesize.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badheadersize.bmp b/image/test/reftest/bmp/bmpsuite/b/badheadersize.bmp new file mode 100644 index 0000000000..2a4083a6f9 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badheadersize.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badpalettesize.bmp b/image/test/reftest/bmp/bmpsuite/b/badpalettesize.bmp new file mode 100644 index 0000000000..7d9d1b745d Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badpalettesize.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badplanes.bmp b/image/test/reftest/bmp/bmpsuite/b/badplanes.bmp new file mode 100644 index 0000000000..92d2855b62 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badplanes.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle.bmp b/image/test/reftest/bmp/bmpsuite/b/badrle.bmp new file mode 100644 index 0000000000..cbf8fdc2e1 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle.png b/image/test/reftest/bmp/bmpsuite/b/badrle.png new file mode 100644 index 0000000000..1764ef9f98 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4.bmp b/image/test/reftest/bmp/bmpsuite/b/badrle4.bmp new file mode 100644 index 0000000000..632787a730 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4.png b/image/test/reftest/bmp/bmpsuite/b/badrle4.png new file mode 100644 index 0000000000..7f5af39fe0 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4bis.bmp b/image/test/reftest/bmp/bmpsuite/b/badrle4bis.bmp new file mode 100644 index 0000000000..130b7e659c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4bis.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4bis.png b/image/test/reftest/bmp/bmpsuite/b/badrle4bis.png new file mode 100644 index 0000000000..7d0d95871a Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4bis.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4ter.bmp b/image/test/reftest/bmp/bmpsuite/b/badrle4ter.bmp new file mode 100644 index 0000000000..8107a335fc Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4ter.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrle4ter.png b/image/test/reftest/bmp/bmpsuite/b/badrle4ter.png new file mode 100644 index 0000000000..230bb60f47 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrle4ter.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrlebis.bmp b/image/test/reftest/bmp/bmpsuite/b/badrlebis.bmp new file mode 100644 index 0000000000..893eb11ab7 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrlebis.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrlebis.png b/image/test/reftest/bmp/bmpsuite/b/badrlebis.png new file mode 100644 index 0000000000..f0094a1156 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrlebis.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrleter.bmp b/image/test/reftest/bmp/bmpsuite/b/badrleter.bmp new file mode 100644 index 0000000000..04d5874e67 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrleter.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badrleter.png b/image/test/reftest/bmp/bmpsuite/b/badrleter.png new file mode 100644 index 0000000000..359bc12e12 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badrleter.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/badwidth.bmp b/image/test/reftest/bmp/bmpsuite/b/badwidth.bmp new file mode 100644 index 0000000000..9fca005dc3 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/badwidth.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/pal1.png b/image/test/reftest/bmp/bmpsuite/b/pal1.png new file mode 100644 index 0000000000..89a433ed76 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/pal1.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/pal8.png b/image/test/reftest/bmp/bmpsuite/b/pal8.png new file mode 100644 index 0000000000..2bfd3e650f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/pal8.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/pal8badindex.bmp b/image/test/reftest/bmp/bmpsuite/b/pal8badindex.bmp new file mode 100644 index 0000000000..efe16c05c1 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/pal8badindex.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/pal8badindex.png b/image/test/reftest/bmp/bmpsuite/b/pal8badindex.png new file mode 100644 index 0000000000..0efb392b9e Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/pal8badindex.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/reallybig.bmp b/image/test/reftest/bmp/bmpsuite/b/reallybig.bmp new file mode 100644 index 0000000000..101e0b4943 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/reallybig.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/reftest.list b/image/test/reftest/bmp/bmpsuite/b/reftest.list new file mode 100644 index 0000000000..c892288b40 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/b/reftest.list @@ -0,0 +1,110 @@ +# bmpsuite "bad" tests + +# See ../README.mozilla for details. + +# BMP: bihsize=40, 127 x 64, bpp=30000, compression=0, colors=2 +# "Header indicates an absurdly large number of bits/pixel." +# [We reject it. So does Chromium.] +== wrapper.html?badbitcount.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "Header incorrectly indicates that the bitmap is several GB in size." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-926) == badbitssize.bmp pal1.png + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "Density (pixels per meter) suggests the image is much larger in one +# dimension than the other." +# [We accept them. So does Chromium.] +== baddens1.bmp pal1.png +== baddens2.bmp pal1.png + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "Header incorrectly indicates that the file is several GB in size." +# [We accept it. So does Chromium.] +== badfilesize.bmp pal1.png + +# BMP: +# "Header size is 66 bytes, which is not a valid size for any known BMP +# version." +# [We reject it. So does Chromium.] +== wrapper.html?badheadersize.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=305402420 +# "Header incorrectly indicates that the palette contains an absurdly large +# number of colors." +# [We reject it. Chromium accepts it but draws nothing. Rejecting seems +# preferable give that the data is clearly untrustworthy.] +fuzzy(0-245,0-8128) == wrapper.html?badpalettesize.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "The 'planes' setting, which is required to be 1, is not 1." +# [We accept it. So does Chromium.] +== badplanes.bmp pal1.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=13 +# "An invalid RLE4-compressed image that tries to cause buffer overruns." +== badrle4.bmp badrle4.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=13 +# "Another invalid RLE4-compressed image that tries to cause buffer overruns." +== badrle4bis.bmp badrle4bis.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=13 +# "Another invalid RLE4-compressed image that tries to cause buffer overruns." +== badrle4ter.bmp badrle4ter.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=253 +# "An invalid RLE-compressed image that tries to cause buffer overruns." +# [We accept it, drawing the valid first part and leaving the rest black. +# Chromium accepts it, drawing the valid first part and leaving the rest +# transparent. Using black for the invalid part is arguably better because it +# makes the image edges more obvious.] +== badrle.bmp badrle.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=253 +# "8-bit version of b/badrle4bis.bmp." +== badrlebis.bmp badrlebis.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=253 +# "8-bit version of b/badrle4ter.bmp." +== badrleter.bmp badrleter.png + +# BMP: bihsize=40, -127 x 64, bpp=1, compression=0, colors=2 +# "The image claims to be a negative number of pixels in width." +# [We reject it. So does Chromium.] +== wrapper.html?badwidth.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=101 +# "Many of the palette indices used in the image are not present in the +# palette." +# [We accept it and use black for the missing colors. So does Chromium.] +== pal8badindex.bmp pal8badindex.png + +# BMP: bihsize=40, 3000000 x 2000000, bpp=24, compression=0, colors=0 +# "An image with a very large reported width and height." +# [We reject it. So does Chromium.] +== wrapper.html?reallybig.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=0 +# "A 16-bit image with a BITFIELDS segment indicating 8 red, 8 green, and 0 +# blue bits. The documentation doesn’t say whether undefined channels are +# legal, or how they should be handled." +== rgb16-880.bmp rgb16-880.png + +# BMP: bihsize=40, 127 x -64, bpp=8, compression=1, colors=252 +# "An RLE-compressed image that tries to use top-down orientation, which isn’t +# allowed." +# [We accept it. Chromium rejects it. Accepting seems better given that we can +# decode it perfectly well.] +fuzzy(0-1,0-996) == rletopdown.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "A file that has been truncated in the middle of the bitmap." +# [We accept it, drawing the part that is present and leaving the rest black. +# Chromium draws the part that is present and leaves the rest transparent. +# Using black for the invalid part is arguably better because it makes the +# image edges more obvious.] +== shortfile.bmp shortfile.png + diff --git a/image/test/reftest/bmp/bmpsuite/b/rgb16-880.bmp b/image/test/reftest/bmp/bmpsuite/b/rgb16-880.bmp new file mode 100644 index 0000000000..72e7ce0eaf Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/rgb16-880.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/rgb16-880.png b/image/test/reftest/bmp/bmpsuite/b/rgb16-880.png new file mode 100644 index 0000000000..9450eba2bb Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/rgb16-880.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/rletopdown.bmp b/image/test/reftest/bmp/bmpsuite/b/rletopdown.bmp new file mode 100644 index 0000000000..21a909fda9 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/rletopdown.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/shortfile.bmp b/image/test/reftest/bmp/bmpsuite/b/shortfile.bmp new file mode 100644 index 0000000000..73960797b9 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/shortfile.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/b/shortfile.png b/image/test/reftest/bmp/bmpsuite/b/shortfile.png new file mode 100644 index 0000000000..0ec21d9295 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/b/shortfile.png differ diff --git a/image/test/reftest/bmp/bmpsuite/b/wrapper.html b/image/test/reftest/bmp/bmpsuite/b/wrapper.html new file mode 100644 index 0000000000..22b74c8fc1 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/b/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/bmp/bmpsuite/g/pal1.bmp b/image/test/reftest/bmp/bmpsuite/g/pal1.bmp new file mode 100644 index 0000000000..4776f82778 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal1.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal1.png b/image/test/reftest/bmp/bmpsuite/g/pal1.png new file mode 100644 index 0000000000..89a433ed76 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal1.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal1bg.bmp b/image/test/reftest/bmp/bmpsuite/g/pal1bg.bmp new file mode 100644 index 0000000000..466d0ba727 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal1bg.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal1bg.png b/image/test/reftest/bmp/bmpsuite/g/pal1bg.png new file mode 100644 index 0000000000..20c4bb838f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal1bg.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal1wb.bmp b/image/test/reftest/bmp/bmpsuite/g/pal1wb.bmp new file mode 100644 index 0000000000..56cb93203e Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal1wb.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal4.bmp b/image/test/reftest/bmp/bmpsuite/g/pal4.bmp new file mode 100644 index 0000000000..7fd36303ca Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal4.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal4.png b/image/test/reftest/bmp/bmpsuite/g/pal4.png new file mode 100644 index 0000000000..188bb0499e Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal4.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal4gs.bmp b/image/test/reftest/bmp/bmpsuite/g/pal4gs.bmp new file mode 100644 index 0000000000..813268ca77 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal4gs.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal4gs.png b/image/test/reftest/bmp/bmpsuite/g/pal4gs.png new file mode 100644 index 0000000000..3a9fc855e4 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal4gs.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal4rle.bmp b/image/test/reftest/bmp/bmpsuite/g/pal4rle.bmp new file mode 100644 index 0000000000..a5672aebd6 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal4rle.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8-0.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8-0.bmp new file mode 100644 index 0000000000..ab8815a360 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8-0.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8.bmp new file mode 100644 index 0000000000..96b2f86680 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8.png b/image/test/reftest/bmp/bmpsuite/g/pal8.png new file mode 100644 index 0000000000..2bfd3e650f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8gs.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8gs.bmp new file mode 100644 index 0000000000..66a0d70dc3 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8gs.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8gs.png b/image/test/reftest/bmp/bmpsuite/g/pal8gs.png new file mode 100644 index 0000000000..b33b38e1d3 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8gs.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare-e.png b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare-e.png new file mode 100644 index 0000000000..646665f2d0 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare-e.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.bmp new file mode 100644 index 0000000000..0aa8de04cb Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.png b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.png new file mode 100644 index 0000000000..9648cb6825 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8nonsquare.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8os2.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8os2.bmp new file mode 100644 index 0000000000..14901b3882 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8os2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8rle.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8rle.bmp new file mode 100644 index 0000000000..d43101490f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8rle.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8topdown.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8topdown.bmp new file mode 100644 index 0000000000..4b2f8e019f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8topdown.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8v4.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8v4.bmp new file mode 100644 index 0000000000..34ebb8030c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8v4.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8v5.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8v5.bmp new file mode 100644 index 0000000000..c54647a31a Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8v5.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w124.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8w124.bmp new file mode 100644 index 0000000000..b7cc2d8bf7 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w124.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w124.png b/image/test/reftest/bmp/bmpsuite/g/pal8w124.png new file mode 100644 index 0000000000..f80236df60 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w124.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w125.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8w125.bmp new file mode 100644 index 0000000000..06efed7443 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w125.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w125.png b/image/test/reftest/bmp/bmpsuite/g/pal8w125.png new file mode 100644 index 0000000000..2a45116b91 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w125.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w126.bmp b/image/test/reftest/bmp/bmpsuite/g/pal8w126.bmp new file mode 100644 index 0000000000..112aa9fe67 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w126.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/pal8w126.png b/image/test/reftest/bmp/bmpsuite/g/pal8w126.png new file mode 100644 index 0000000000..a41eab93d0 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/pal8w126.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/reftest.list b/image/test/reftest/bmp/bmpsuite/g/reftest.list new file mode 100644 index 0000000000..d14c5aa42e --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/g/reftest.list @@ -0,0 +1,129 @@ +# bmpsuite "good" tests + +# See ../README.mozilla for details. + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "1 bit/pixel paletted image, in which black is the first color in the +# palette." +fuzzy(0-1,0-926) == pal1.bmp pal1.png + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "1 bit/pixel paletted image, in which white is the first color in the +# palette." +fuzzy(0-1,0-926) == pal1wb.bmp pal1.png + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=2 +# "1 bit/pixel paletted image, with colors other than black and white." +== pal1bg.bmp pal1bg.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=0, colors=12 +# "Paletted image with 12 palette colors, and 4 bits/pixel." +== pal4.bmp pal4.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=0, colors=12 +# "Paletted image with 12 grayscale palette colors, and 4 bits/pixel." +== pal4gs.bmp pal4gs.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=12 +# "4-bit image that uses RLE compression." +== pal4rle.bmp pal4.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=252 +# "Our standard paletted image, with 252 palette colors, and 8 bits/pixel." +fuzzy(0-1,0-996) == pal8.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=0 +# "Every field that can be set to 0 is set to 0: pixels/meter=0; colors used=0 +# (meaning the default 256); size-of-image=0." +fuzzy(0-1,0-996) == pal8-0.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=252 +# "An 8-bit image with a palette of 252 grayscale colors." +== pal8gs.bmp pal8gs.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=252 +# "8-bit image that uses RLE compression." +fuzzy(0-1,0-996) == pal8rle.bmp pal8.png + +# BMP: bihsize=40, 126 x 63, bpp=8, compression=0, colors=252 +# BMP: bihsize=40, 125 x 62, bpp=8, compression=0, colors=252 +# BMP: bihsize=40, 124 x 61, bpp=8, compression=0, colors=252 +# "Images with different widths and heights. In BMP format, rows are padded to +# a multiple of four bytes, so we test all four possibilities." +fuzzy(0-1,0-889) == pal8w126.bmp pal8w126.png +fuzzy(0-1,0-974) == pal8w125.bmp pal8w125.png +fuzzy(0-1,0-870) == pal8w124.bmp pal8w124.png + +# BMP: bihsize=40, 127 x -64, bpp=8, compression=0, colors=252 +# "BMP images are normally stored from the bottom up, but there is a way to +# store them from the top down." +fuzzy(0-1,0-996) == pal8topdown.bmp pal8.png + +# BMP: bihsize=40, 127 x 32, bpp=8, compression=0, colors=252 +# "An image with non-square pixels: the X pixels/meter is twice the Y +# pixels/meter. Image editors can be expected to leave the image 'squashed'; +# image viewers should consider stretching it to its correct proportions." +# [We leave it squashed, as does Chromium.] +fuzzy(0-1,0-1462) == pal8nonsquare.bmp pal8nonsquare-e.png + +# BMP: bihsize=12, 127 x 64, bpp=8, compression=0, colors=0 +# "An OS/2-style bitmap." +fuzzy(0-1,0-996) == pal8os2.bmp pal8.png + +# BMP: bihsize=108, 127 x 64, bpp=8, compression=0, colors=252 +# "A v4 bitmap. I’m not sure that the gamma and chromaticity values in this +# file are sensible, because I can’t find any detailed documentation of them." +# [We seem to handle the profile wrong in QCMS. See bug 1619332.] +fuzzy-if((/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)),1-4,899-6376) fuzzy-if(!(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)),3-3,6376-6376) fuzzy-if(appleSilicon,4-4,6376-6376) == pal8v4.bmp pal8.png #bug 1619847 + +# BMP: bihsize=124, 127 x 64, bpp=8, compression=0, colors=252 +# "A v5 bitmap. Version 5 has additional colorspace options over v4, so it is +# easier to create, and ought to be more portable." +fuzzy(0-1,0-996) == pal8v5.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=0, colors=0 +# "A 16-bit image with the default color format: 5 bits each for red, green, and +# blue, and 1 unused bit. The whitest colors should (I assume) be displayed as +# pure white: (255,255,255), not (248,248,248)." +fuzzy(0-1,0-1296) == rgb16.bmp rgb16.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=0 +# "Same format as rgb16.bmp, but with a BITFIELDS segment." +fuzzy(0-1,0-1296) == rgb16bfdef.bmp rgb16.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=0 +# "A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green, and 5 blue +# bits. This is a standard 16-bit format, even supported by old versions of +# Windows that don’t support any other non-default 16-bit formats. The whitest +# colors should be displayed as pure white: (255,255,255), not (248,252,248)." +fuzzy(0-1,0-1296) == rgb16.bmp rgb16.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=256 +# "A 16-bit image with both a BITFIELDS segment and a palette." +fuzzy(0-1,0-1516) == rgb16.bmp rgb16.png + +# BMP: bihsize=40, 127 x 64, bpp=24, compression=0, colors=0 +# "A perfectly ordinary 24-bit (truecolor) image." +== rgb24.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=24, compression=0, colors=256 +# "A 24-bit image, with a palette containing 256 colors. There is little if any +# reason for a truecolor image to contain a palette, but it is legal." +== rgb24pal.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=0, colors=0 +# "A 32-bit image using the default color format for 32-bit images (no +# BITFIELDS segment). There are 8 bits per color channel, and 8 unused bits. +# The unused bits are set to 0." +== rgb32.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=3, colors=0 +# "Same format as rgb32.bmp, but with a BITFIELDS segment." +== rgb32bfdef.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per color +# channel, and 8 unused bits. But the color channels are in an unusual order, +# so the viewer must read the BITFIELDS, and not just guess." +== rgb32bf.bmp rgb24.png + diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16-565.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb16-565.bmp new file mode 100644 index 0000000000..c03a27975a Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16-565.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16-565.png b/image/test/reftest/bmp/bmpsuite/g/rgb16-565.png new file mode 100644 index 0000000000..04a3121d24 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16-565.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16-565pal.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb16-565pal.bmp new file mode 100644 index 0000000000..e7632e344b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16-565pal.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb16.bmp new file mode 100644 index 0000000000..6bfe47af4f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16.png b/image/test/reftest/bmp/bmpsuite/g/rgb16.png new file mode 100644 index 0000000000..d9545840ab Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb16bfdef.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb16bfdef.bmp new file mode 100644 index 0000000000..30fe8bb8d6 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb16bfdef.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb24.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb24.bmp new file mode 100644 index 0000000000..40f8bb094b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb24.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb24.png b/image/test/reftest/bmp/bmpsuite/g/rgb24.png new file mode 100644 index 0000000000..86a9c945b0 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb24.png differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb24pal.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb24pal.bmp new file mode 100644 index 0000000000..102e971dd3 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb24pal.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb32.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb32.bmp new file mode 100644 index 0000000000..5d57eaaea8 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb32.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb32bf.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb32bf.bmp new file mode 100644 index 0000000000..20fa9a1326 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb32bf.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/g/rgb32bfdef.bmp b/image/test/reftest/bmp/bmpsuite/g/rgb32bfdef.bmp new file mode 100644 index 0000000000..d7e64e5a41 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/g/rgb32bfdef.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal1huff.bmp b/image/test/reftest/bmp/bmpsuite/q/pal1huff.bmp new file mode 100644 index 0000000000..790a483697 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal1huff.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal1p1.bmp b/image/test/reftest/bmp/bmpsuite/q/pal1p1.bmp new file mode 100644 index 0000000000..b68321c4c1 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal1p1.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal1p1.png b/image/test/reftest/bmp/bmpsuite/q/pal1p1.png new file mode 100644 index 0000000000..92fc0f945b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal1p1.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal2.bmp b/image/test/reftest/bmp/bmpsuite/q/pal2.bmp new file mode 100644 index 0000000000..983e9fa92d Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal2color.bmp b/image/test/reftest/bmp/bmpsuite/q/pal2color.bmp new file mode 100644 index 0000000000..27fe7276bc Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal2color.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.bmp b/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.bmp new file mode 100644 index 0000000000..2f32d1d7ad Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.png b/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.png new file mode 100644 index 0000000000..767f5a0ad7 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal4rlecut.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.bmp b/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.bmp new file mode 100644 index 0000000000..58994e92ba Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.png b/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.png new file mode 100644 index 0000000000..9b0c044364 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal4rletrns.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8.png b/image/test/reftest/bmp/bmpsuite/q/pal8.png new file mode 100644 index 0000000000..2bfd3e650f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8offs.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8offs.bmp new file mode 100644 index 0000000000..8673e9740b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8offs.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2-hs.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2-hs.bmp new file mode 100644 index 0000000000..018a3c4b16 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2-hs.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2-sz.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2-sz.bmp new file mode 100644 index 0000000000..7f1455d5ef Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2-sz.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2sp.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2sp.bmp new file mode 100644 index 0000000000..e532c89863 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2sp.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-16.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-16.bmp new file mode 100644 index 0000000000..95a1d2345a Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-16.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-40sz.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-40sz.bmp new file mode 100644 index 0000000000..d1e66b615c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-40sz.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-sz.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-sz.bmp new file mode 100644 index 0000000000..6fe566e3fd Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2-sz.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8os2v2.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2.bmp new file mode 100644 index 0000000000..1324a40d00 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8os2v2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8oversizepal.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8oversizepal.bmp new file mode 100644 index 0000000000..93b8187ca1 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8oversizepal.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.bmp new file mode 100644 index 0000000000..840d31cce6 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.png b/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.png new file mode 100644 index 0000000000..dfceeb568b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8rlecut.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.bmp b/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.bmp new file mode 100644 index 0000000000..a2af88d87c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.png b/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.png new file mode 100644 index 0000000000..2d8e957f1f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/pal8rletrns.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/reftest.list b/image/test/reftest/bmp/bmpsuite/q/reftest.list new file mode 100644 index 0000000000..cead5df00b --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/q/reftest.list @@ -0,0 +1,251 @@ +# bmpsuite "questionable" tests + +# See ../README.mozilla for details. + +# BMP: bihsize=40, 127 x 64, bpp=1, compression=0, colors=1 +# "1 bit/pixel paletted image, with only one color in the palette. The +# documentation says that 1-bpp images have a palette size of 2 (not 'up to +# 2'), but it would be silly for a viewer not to support a size of 1." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-926) == pal1p1.bmp pal1p1.png + +# BMP: bihsize=64, 127 x 64, bpp=1, compression=3, colors=2 +# "My attempt to make a BMP file with Huffman 1D compression. It is quite +# possibly incorrect. Even if everything else about it is correct, I have no +# way to know whether it is black/white reversed." +# [We reject it. So does Chromium.] +== wrapper.html?pal1huff.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=2, compression=0, colors=4 +# "A paletted image with 2 bits/pixel. Usually only 1, 4, and 8 are allowed, +# but 2 is legal on Windows CE." +# [We reject it. Chromium accepts it.] +== wrapper.html?pal2.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=2, compression=0, colors=4 +# "Same as pal2.bmp, but with a color palette instead of grayscale palette." +# [We reject it. Chromium accepts it.] +== wrapper.html?pal2color.bmp about:blank + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=13 +# "An RLE-compressed image that used 'delta' codes to skip over some pixels, +# leaving them undefined. Some viewers make undefined pixels transparent, +# others make them black, and others assign them palette color 0 (purple, in +# this case)." +# [We make the undefined pixels transparent. So does Chromium.] +== pal4rletrns.bmp pal4rletrns.png + +# BMP: bihsize=40, 127 x 64, bpp=4, compression=2, colors=13 +# "An RLE-compressed image that uses “delta†codes, and early EOL & EOBMP +# markers, to skip over some pixels." +== pal4rlecut.bmp pal4rlecut.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=253 +# "8-bit version of q/pal4rletrns.bmp." +# [Ditto.] +== pal8rletrns.bmp pal8rletrns.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=1, colors=253 +# "8-bit version of q/pal4rlecut.bmp." +== pal8rlecut.bmp pal8rlecut.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=252 +# "A file with some unused bytes between the palette and the image. This is +# probably valid, but I’m not 100% sure." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-996) == pal8offs.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=300 +# "An 8-bit image with 300 palette colors. This may be invalid, because the +# documentation could be interpreted to imply that 8-bit images aren’t allowed +# to have more than 256 colors." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-996) == pal8oversizepal.bmp pal8.png + +# BMP: bihsize=12, 127 x 64, bpp=8, compression=0, colors=0 +# # "Some OS/2 BMP specifications say that the size field in the file header +# should be set to the aggregate size of the file header and infoheader, +# instead of the total file size. For OS/2v1, that means it will always be 26. +# BMP decoders usually ignore this field, so it shouldn’t cause a problem." +fuzzy(0-1,0-996) == pal8os2-sz.bmp pal8.png + +# BMP: bihsize=12, 127 x 64, bpp=8, compression=0, colors=0 +# "Some OS/2 BMP specifications define the fields at offsets 6 and 8 to be a +# “hotspot†(for cursor graphics). Though the fields are not used in BMP files, +# they are sometimes, as in this file, set to nonzero values. This should cause +# no problems, except that it could prevent some programs from detecting this +# file as a BMP file." +fuzzy(0-1,0-996) == pal8os2-hs.bmp pal8.png + +# BMP: bihsize=12, 127 x 64, bpp=8, compression=0, colors=0 +# "An OS/2v1 with a less-than-full-sized palette. Probably not valid, but such +# files have been seen in the wild." +# [We reject it. Chromium accepts it.] +fuzzy(0-245,0-8128) == wrapper.html?pal8os2sp.bmp about:blank + +# BMP: bihsize=64, 127 x 64, bpp=8, compression=0, colors=252 +# "My attempt to make an OS/2v2 bitmap." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-996) == pal8os2v2.bmp pal8.png + +# BMP: bihsize=16, 127 x 64, bpp=8, compression=0, colors=0 +# "An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64." +# [We accept it. So does Chromium.] +fuzzy(0-1,0-996) == pal8os2v2-16.bmp pal8.png + +# BMP: bihsize=64, 127 x 64, bpp=8, compression=0, colors=252 +# "An OS/2v2 bitmap. Like q/pal8os2-sz.bmp, the size field is set to the size +# of the headers (78), instead of the size of the file." +fuzzy(0-1,0-996) == pal8os2v2-sz.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=8, compression=0, colors=252 +# "An OS/2v2 bitmap, with a 40-byte header. Like q/pal8os2-sz.bmp, the size +# field is set to the size of the headers (54), instead of the size of the +# file. Except for that, this file cannot be distinguished from a Windows +# BMPv3 file." +fuzzy(0-1,0-996) == pal8os2v2-40sz.bmp pal8.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=0, colors=0 +# "Same idea as q/rgb32fakealpha.bmp. The default 16-bit color format has one +# unused bit per pixel, and in this image some of the unused bits are set to 1. +# It’s possible that some viewers will interpret this image as having +# transparency." +fuzzy(0-1,0-1296) == rgb16faketrns.bmp rgb16.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=0 +# "An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1 blue +# bit. Most viewers do support this image, but the colors may be darkened with +# a yellow-green shadow. That’s because they’re doing simple bit-shifting +# (possibly including one round of bit replication), instead of proper +# scaling." +== rgb16-231.bmp rgb16-231.png + +# BMP: bihsize=40, 127 x 64, bpp=16, compression=3, colors=0 +# "Similar to q/rgb16-231.bmp, with 3 red bits, 10 green bits, and 3 blue +# bits." +fuzzy(0-1,0-689) == rgb16-3103.bmp rgb16-3103.png + +# BMP: bihsize=124, 127 x 64, bpp=16, compression=3, colors=0 +# "A 16-bit image with an alpha channel. There are 4 bits for each color +# channel, and 4 bits for the alpha channel. It’s not clear if this is valid, +# but I can’t find anything that suggests it isn’t." +== rgba16-4444.bmp rgba16-4444.png + +# BMP: bihsize=124, 127 x 64, bpp=16, compression=3, colors=0 +# "Similar to q/rgba16-4444.bmp, with 5 red bits, 5 green bits, 5 blue bits, +# and a 1-bit alpha channel." +fuzzy(0-1,0-2203) == rgba16-5551.bmp rgba16-5551.png + +# BMP: bihsize=124, 127 x 64, bpp=16, compression=3, colors=0 +# "Similar to q/rgba16-4444.bmp, with 1 red bit, 9 green bits, 2 blue bits, +# and 4 bits for the alpha channel." +== rgba16-1924.bmp rgba16-1924.png + +# BMP: bihsize=40, 127 x 64, bpp=24, compression=0, colors=300 +# "A 24-bit image, with a palette containing 300 colors. The fact that the +# palette has more than 256 colors may cause some viewers to complain, but the +# documentation does not mention a size limit." +# [We accept it. So does Chromium.] +== rgb24largepal.bmp rgb24.png + +# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0 +# "My attempt to make a BMP file with an embedded color profile." +fuzzy(1-1,28-73) == rgb24prof.bmp rgb24.png + +# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0 +# "This image tries to test whether color profiles are fully supported. It has +# the red and green channels swapped, and an embedded color profile that tries +# to swap them back. Support for this is uncommon." +# [The image is significantly closer to the desired output than without color +# management, but we seem to handle the profile wrong in QCMS. See bug 1619332.] +fuzzy(10-10,6590-6597) == rgb24prof2.bmp rgb24.png + +# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0 +# "My attempt to make a BMP file with a linked color profile." +# [We accept it, though we don't do anything with the color profile. Chromium +# also handles it.] +== rgb24lprof.bmp rgb24.png + +# BMP: bihsize=124, 127 x 64, bpp=0, compression=4, colors=0 +# BMP: bihsize=124, 127 x 64, bpp=0, compression=5, colors=0 +# "My attempt to make BMP files with embedded JPEG and PNG images. These are +# not likely to be supported by much of anything (they’re intended for +# printers)." +# [We reject them. Chromium accepts them.] +== wrapper.html?rgb24jpeg.bmp about:blank +== wrapper.html?rgb24png.bmp about:blank + +# BMP: bihsize=64, 127 x 64, bpp=24, compression=4, colors=0 +# "An OS/2v2 bitmap with RLE24 compression. This image uses a limited number +# of colors, just to make it more compressible." +# [We reject it. Chromium accepts it.] +== wrapper.html?rgb24rle24.bmp about:blank + +# BMP: bihsize=52, 127 x 64, bpp=32, compression=3, colors=0 +# "Similar to g/rgb32bf.bmp, but with a 52-byte “BITMAPV2INFOHEADERâ€. This is +# an uncommon version of BMP, and I can’t confirm that this file is correct." +# [We reject it. Chromium accepts it.] +== wrapper.html?rgb32h52.bmp about:blank + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "Color channels are the same size and order as rgb32bfdef.bmp, but they use +# the highest available bits, instead of the lowest (or vice versa, depending +# on your byte-order perspective)." +== rgb32-xbgr.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=0, colors=0 +# "Same as g/rgb32.bmp, except that the unused bits are set to something other +# than 0. If the image becomes transparent toward the bottom, it probably means +# the viewer uses heuristics to guess whether the undefined data represents +# transparency." +# [We don't apply transparency here. Chromium does the same.] +== rgb32fakealpha.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32 bits/pixel image, with all 32 bits used: 11 each for red and green, and +# 10 for blue. As far as I know, this is perfectly valid, but it is unusual." +fuzzy(0-1,0-1408) == rgb32-111110.bmp rgb24.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32 bits/pixel image, with 7 bits for red, 18 for green, and 7 for blue." +fuzzy(0-1,0-753) == rgb32-7187.bmp rgb32-7187.png + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "A BMP with an alpha channel. Transparency is barely documented, so it’s +# possible that this file is not correctly formed. The color channels are in an +# unusual order, to prevent viewers from passing this test by making a lucky +# guess." +== rgba32-1.bmp rgba32.png + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "Same as rgba32-1.bmp, but with the color channels in an unusual order, to +# prevent viewers from passing this test by making a lucky guess." +== rgba32-2.bmp rgba32.png + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32 bits/pixel image, with 10 bits for red, 10 for green, 10 for blue, and +# 2 for alpha." +fuzzy(0-1,0-1296) == rgba32-1010102.bmp rgba32-1010102.png + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32 bits/pixel image, with 8 bits for red, 12 for green, 8 for blue, and 4 +# for alpha." +fuzzy(0-1,0-753) == rgba32-81284.bmp rgba32-81284.png + +# BMP: bihsize=124, 127 x 64, bpp=32, compression=3, colors=0 +# "A 32 bits/pixel image, with 6 bits for red, 17 for green, 5 for blue, and 4 +# for alpha." +fuzzy(0-1,0-1554) == rgba32-61754.bmp rgba32-61754.png + +# BMP: bihsize=40, 127 x 64, bpp=32, compression=6, colors=0 +# "An image of type BI_ALPHABITFIELDS. Supposedly, this was used on Windows CE. +# I don’t know whether it is constructed correctly." +# [We reject it. Chromium accepts it.] +== wrapper.html?rgba32abf.bmp about:blank + +# BMP: bihsize=56, 127 x 64, bpp=32, compression=3, colors=0 +# "Similar to q/rgba32.bmp, but with a 56-byte “BITMAPV3INFOHEADERâ€. This is an +# uncommon version of BMP, and I can’t confirm that this file is correct." +# [We reject it. Chromium accepts it.] +== wrapper.html?rgba32h56.bmp about:blank + diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16-231.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb16-231.bmp new file mode 100644 index 0000000000..6300f69f0c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16-231.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16-231.png b/image/test/reftest/bmp/bmpsuite/q/rgb16-231.png new file mode 100644 index 0000000000..76efe526e5 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16-231.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.bmp new file mode 100644 index 0000000000..6e01226029 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.png b/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.png new file mode 100644 index 0000000000..79ba23c834 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16-3103.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16.png b/image/test/reftest/bmp/bmpsuite/q/rgb16.png new file mode 100644 index 0000000000..d9545840ab Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb16faketrns.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb16faketrns.bmp new file mode 100644 index 0000000000..62fb393bb9 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb16faketrns.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24.png b/image/test/reftest/bmp/bmpsuite/q/rgb24.png new file mode 100644 index 0000000000..86a9c945b0 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24jpeg.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24jpeg.bmp new file mode 100644 index 0000000000..87d73d75b8 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24jpeg.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24largepal.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24largepal.bmp new file mode 100644 index 0000000000..d5e418c2d4 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24largepal.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24lprof.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24lprof.bmp new file mode 100644 index 0000000000..b868b88f20 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24lprof.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24png.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24png.bmp new file mode 100644 index 0000000000..e87ec7adda Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24png.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24prof.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24prof.bmp new file mode 100644 index 0000000000..627e676eae Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24prof.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24prof2.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24prof2.bmp new file mode 100644 index 0000000000..f9f61b8ee3 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24prof2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24rle24.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb24rle24.bmp new file mode 100644 index 0000000000..360aee649c Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb24rle24.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32-111110.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb32-111110.bmp new file mode 100644 index 0000000000..ec07d89b5b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32-111110.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.bmp new file mode 100644 index 0000000000..887ba52c91 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.png b/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.png new file mode 100644 index 0000000000..a1da44d6be Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32-7187.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32-xbgr.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb32-xbgr.bmp new file mode 100644 index 0000000000..c6c05e1480 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32-xbgr.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32fakealpha.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb32fakealpha.bmp new file mode 100644 index 0000000000..cb544da5b6 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32fakealpha.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb32h52.bmp b/image/test/reftest/bmp/bmpsuite/q/rgb32h52.bmp new file mode 100644 index 0000000000..db6e4538ef Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgb32h52.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.bmp new file mode 100644 index 0000000000..6564098a4d Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.png b/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.png new file mode 100644 index 0000000000..0fc182f153 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-1924.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.bmp new file mode 100644 index 0000000000..051ff23589 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.png b/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.png new file mode 100644 index 0000000000..bfeda6faed Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-4444.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.bmp new file mode 100644 index 0000000000..73e2cd533f Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.png b/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.png new file mode 100644 index 0000000000..613126a8ab Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba16-5551.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-1.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32-1.bmp new file mode 100644 index 0000000000..3c1e2648fc Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-1.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.bmp new file mode 100644 index 0000000000..1a918cebf5 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.png b/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.png new file mode 100644 index 0000000000..a472fbc799 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-1010102.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-2.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32-2.bmp new file mode 100644 index 0000000000..829c7c7e34 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-2.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.bmp new file mode 100644 index 0000000000..d5936fd20b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.png b/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.png new file mode 100644 index 0000000000..c593b14c66 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-61754.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.bmp new file mode 100644 index 0000000000..1f9fc29d6b Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.png b/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.png new file mode 100644 index 0000000000..c958cd36fc Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32-81284.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32.png b/image/test/reftest/bmp/bmpsuite/q/rgba32.png new file mode 100644 index 0000000000..25e542a655 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32.png differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32abf.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32abf.bmp new file mode 100644 index 0000000000..d9bb0189c4 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32abf.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/rgba32h56.bmp b/image/test/reftest/bmp/bmpsuite/q/rgba32h56.bmp new file mode 100644 index 0000000000..343baa3300 Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/q/rgba32h56.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/q/wrapper.html b/image/test/reftest/bmp/bmpsuite/q/wrapper.html new file mode 100644 index 0000000000..22b74c8fc1 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/q/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/bmp/bmpsuite/reftest.list b/image/test/reftest/bmp/bmpsuite/reftest.list new file mode 100644 index 0000000000..532ea5ed93 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/reftest.list @@ -0,0 +1,8 @@ +# bmpsuite tests + +# See README.mozilla for details about these tests. + +include g/reftest.list +include q/reftest.list +include b/reftest.list +include x/reftest.list diff --git a/image/test/reftest/bmp/bmpsuite/x/ba-bm.bmp b/image/test/reftest/bmp/bmpsuite/x/ba-bm.bmp new file mode 100644 index 0000000000..d2615bde3e Binary files /dev/null and b/image/test/reftest/bmp/bmpsuite/x/ba-bm.bmp differ diff --git a/image/test/reftest/bmp/bmpsuite/x/reftest.list b/image/test/reftest/bmp/bmpsuite/x/reftest.list new file mode 100644 index 0000000000..3c93479292 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/x/reftest.list @@ -0,0 +1,10 @@ +# bmpsuite 'files in formats related to BMP, that you might not consider to +# truly be in "BMP format"' tests + +# See ../README.mozilla for details. + +# "This image uses the OS/2v2 “Bitmap Array†(BA) container format. Although a +# BA file may contain multiple images, this file has only one." +== wrapper.html?ba-bm.bmp about:blank + + diff --git a/image/test/reftest/bmp/bmpsuite/x/wrapper.html b/image/test/reftest/bmp/bmpsuite/x/wrapper.html new file mode 100644 index 0000000000..f6c3ade538 --- /dev/null +++ b/image/test/reftest/bmp/bmpsuite/x/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/bmp/reftest.list b/image/test/reftest/bmp/reftest.list new file mode 100644 index 0000000000..87183e6b2f --- /dev/null +++ b/image/test/reftest/bmp/reftest.list @@ -0,0 +1,16 @@ +# BMP tests + +include bmp-1bpp/reftest.list +include bmp-4bpp/reftest.list +include bmp-8bpp/reftest.list +include bmp-24bpp/reftest.list +include bmp-corrupted/reftest.list +include bmpsuite/reftest.list + +# Two bmp files where the offset to the start of the image data in the file +# is past the end of the file. In 1240629-1.bmp the offset us uint32_max, +# so we are testing that we don't try to allocate a buffer that size (and +# fail on 32 bit platforms) and declare the image in error state. If in the +# future we decide that such bmps (offset past the end of the file) are +# invalid the test will still pass, but won't be testing much. +== 1240629-1.bmp 1240629-2.bmp diff --git a/image/test/reftest/color-management/color-curv.png b/image/test/reftest/color-management/color-curv.png new file mode 100644 index 0000000000..994e3a38a6 Binary files /dev/null and b/image/test/reftest/color-management/color-curv.png differ diff --git a/image/test/reftest/color-management/color-lin.png b/image/test/reftest/color-management/color-lin.png new file mode 100644 index 0000000000..0ee276fca3 Binary files /dev/null and b/image/test/reftest/color-management/color-lin.png differ diff --git a/image/test/reftest/color-management/color-table.png b/image/test/reftest/color-management/color-table.png new file mode 100644 index 0000000000..355b3a2ba6 Binary files /dev/null and b/image/test/reftest/color-management/color-table.png differ diff --git a/image/test/reftest/color-management/invalid-chrm-ref.png b/image/test/reftest/color-management/invalid-chrm-ref.png new file mode 100644 index 0000000000..85f83f7834 Binary files /dev/null and b/image/test/reftest/color-management/invalid-chrm-ref.png differ diff --git a/image/test/reftest/color-management/invalid-chrm.png b/image/test/reftest/color-management/invalid-chrm.png new file mode 100644 index 0000000000..33dc9e9ce0 Binary files /dev/null and b/image/test/reftest/color-management/invalid-chrm.png differ diff --git a/image/test/reftest/color-management/invalid-whitepoint.png b/image/test/reftest/color-management/invalid-whitepoint.png new file mode 100644 index 0000000000..383a0a035f Binary files /dev/null and b/image/test/reftest/color-management/invalid-whitepoint.png differ diff --git a/image/test/reftest/color-management/reftest.list b/image/test/reftest/color-management/reftest.list new file mode 100644 index 0000000000..a99b4d4391 --- /dev/null +++ b/image/test/reftest/color-management/reftest.list @@ -0,0 +1,7 @@ +# Colormangement + +# test for bug 489133, test for bug 460520 +== invalid-chrm.png invalid-chrm-ref.png +== invalid-whitepoint.png invalid-chrm-ref.png +# test for bug 488955 +== trc-type.html trc-type-ref.html # Bug 1560617 diff --git a/image/test/reftest/color-management/trc-type-ref.html b/image/test/reftest/color-management/trc-type-ref.html new file mode 100644 index 0000000000..5140e6e6af --- /dev/null +++ b/image/test/reftest/color-management/trc-type-ref.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/image/test/reftest/color-management/trc-type.html b/image/test/reftest/color-management/trc-type.html new file mode 100644 index 0000000000..f13052bbf4 --- /dev/null +++ b/image/test/reftest/color-management/trc-type.html @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/image/test/reftest/colordepth.html b/image/test/reftest/colordepth.html new file mode 100644 index 0000000000..15d2c9f95f --- /dev/null +++ b/image/test/reftest/colordepth.html @@ -0,0 +1,16 @@ + diff --git a/image/test/reftest/downscaling/100x100.gif b/image/test/reftest/downscaling/100x100.gif new file mode 100644 index 0000000000..4ff770d368 Binary files /dev/null and b/image/test/reftest/downscaling/100x100.gif differ diff --git a/image/test/reftest/downscaling/100x100.jpg b/image/test/reftest/downscaling/100x100.jpg new file mode 100644 index 0000000000..cea2c240d8 Binary files /dev/null and b/image/test/reftest/downscaling/100x100.jpg differ diff --git a/image/test/reftest/downscaling/100x100.png b/image/test/reftest/downscaling/100x100.png new file mode 100644 index 0000000000..eb3cb08e1e Binary files /dev/null and b/image/test/reftest/downscaling/100x100.png differ diff --git a/image/test/reftest/downscaling/100x32768.gif b/image/test/reftest/downscaling/100x32768.gif new file mode 100644 index 0000000000..f55a2f482f Binary files /dev/null and b/image/test/reftest/downscaling/100x32768.gif differ diff --git a/image/test/reftest/downscaling/100x32768.jpg b/image/test/reftest/downscaling/100x32768.jpg new file mode 100644 index 0000000000..e89acc0334 Binary files /dev/null and b/image/test/reftest/downscaling/100x32768.jpg differ diff --git a/image/test/reftest/downscaling/100x32768.png b/image/test/reftest/downscaling/100x32768.png new file mode 100644 index 0000000000..7cd5509cf2 Binary files /dev/null and b/image/test/reftest/downscaling/100x32768.png differ diff --git a/image/test/reftest/downscaling/1404366-1.html b/image/test/reftest/downscaling/1404366-1.html new file mode 100644 index 0000000000..165cc7f934 --- /dev/null +++ b/image/test/reftest/downscaling/1404366-1.html @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/image/test/reftest/downscaling/1404366-1.ico b/image/test/reftest/downscaling/1404366-1.ico new file mode 100644 index 0000000000..51c020b069 Binary files /dev/null and b/image/test/reftest/downscaling/1404366-1.ico differ diff --git a/image/test/reftest/downscaling/1421191-1.html b/image/test/reftest/downscaling/1421191-1.html new file mode 100644 index 0000000000..b8146a2371 --- /dev/null +++ b/image/test/reftest/downscaling/1421191-1.html @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/image/test/reftest/downscaling/1421191-1.png b/image/test/reftest/downscaling/1421191-1.png new file mode 100644 index 0000000000..e9b756a79e Binary files /dev/null and b/image/test/reftest/downscaling/1421191-1.png differ diff --git a/image/test/reftest/downscaling/32768x100.gif b/image/test/reftest/downscaling/32768x100.gif new file mode 100644 index 0000000000..2194d13efc Binary files /dev/null and b/image/test/reftest/downscaling/32768x100.gif differ diff --git a/image/test/reftest/downscaling/32768x100.jpg b/image/test/reftest/downscaling/32768x100.jpg new file mode 100644 index 0000000000..807b9adb27 Binary files /dev/null and b/image/test/reftest/downscaling/32768x100.jpg differ diff --git a/image/test/reftest/downscaling/32768x100.png b/image/test/reftest/downscaling/32768x100.png new file mode 100644 index 0000000000..3fe4fe1ce5 Binary files /dev/null and b/image/test/reftest/downscaling/32768x100.png differ diff --git a/image/test/reftest/downscaling/black-border-bottom.png b/image/test/reftest/downscaling/black-border-bottom.png new file mode 100644 index 0000000000..efa7ce2dce Binary files /dev/null and b/image/test/reftest/downscaling/black-border-bottom.png differ diff --git a/image/test/reftest/downscaling/black-border-left.png b/image/test/reftest/downscaling/black-border-left.png new file mode 100644 index 0000000000..11bc67e983 Binary files /dev/null and b/image/test/reftest/downscaling/black-border-left.png differ diff --git a/image/test/reftest/downscaling/black-border-rect.svg b/image/test/reftest/downscaling/black-border-rect.svg new file mode 100644 index 0000000000..0fa01a0a63 --- /dev/null +++ b/image/test/reftest/downscaling/black-border-rect.svg @@ -0,0 +1,3 @@ + + + diff --git a/image/test/reftest/downscaling/black-border-right.png b/image/test/reftest/downscaling/black-border-right.png new file mode 100644 index 0000000000..081c52d5bc Binary files /dev/null and b/image/test/reftest/downscaling/black-border-right.png differ diff --git a/image/test/reftest/downscaling/black-border-top.png b/image/test/reftest/downscaling/black-border-top.png new file mode 100644 index 0000000000..fc6e69e02a Binary files /dev/null and b/image/test/reftest/downscaling/black-border-top.png differ diff --git a/image/test/reftest/downscaling/bmp-size-16x16-24bpp.png b/image/test/reftest/downscaling/bmp-size-16x16-24bpp.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/downscaling/bmp-size-16x16-24bpp.png differ diff --git a/image/test/reftest/downscaling/downscale-1-bigimage.png b/image/test/reftest/downscaling/downscale-1-bigimage.png new file mode 100644 index 0000000000..5e018590c3 Binary files /dev/null and b/image/test/reftest/downscaling/downscale-1-bigimage.png differ diff --git a/image/test/reftest/downscaling/downscale-1-ref.html b/image/test/reftest/downscaling/downscale-1-ref.html new file mode 100644 index 0000000000..63ec56ef7c --- /dev/null +++ b/image/test/reftest/downscaling/downscale-1-ref.html @@ -0,0 +1,8 @@ + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-1-smallimage.png b/image/test/reftest/downscaling/downscale-1-smallimage.png new file mode 100644 index 0000000000..588e6b78df Binary files /dev/null and b/image/test/reftest/downscaling/downscale-1-smallimage.png differ diff --git a/image/test/reftest/downscaling/downscale-1.html b/image/test/reftest/downscaling/downscale-1.html new file mode 100644 index 0000000000..a9629ef855 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-1.html @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-16px.html b/image/test/reftest/downscaling/downscale-16px.html new file mode 100644 index 0000000000..06d6db2bf6 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-16px.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2a.html b/image/test/reftest/downscaling/downscale-2a.html new file mode 100644 index 0000000000..fac11ccee5 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2a.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2b.html b/image/test/reftest/downscaling/downscale-2b.html new file mode 100644 index 0000000000..af7ecbff33 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2b.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2c.html b/image/test/reftest/downscaling/downscale-2c.html new file mode 100644 index 0000000000..18f70456b1 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2c.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2d.html b/image/test/reftest/downscaling/downscale-2d.html new file mode 100644 index 0000000000..8d9547b73e --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2d.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2e.html b/image/test/reftest/downscaling/downscale-2e.html new file mode 100644 index 0000000000..c3d0d771f3 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2e.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-2f.html b/image/test/reftest/downscaling/downscale-2f.html new file mode 100644 index 0000000000..42cfad1f57 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-2f.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-32px-ref.html b/image/test/reftest/downscaling/downscale-32px-ref.html new file mode 100644 index 0000000000..1caf3c73bf --- /dev/null +++ b/image/test/reftest/downscaling/downscale-32px-ref.html @@ -0,0 +1,8 @@ + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-32px.html b/image/test/reftest/downscaling/downscale-32px.html new file mode 100644 index 0000000000..f5fce324d9 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-32px.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-8px.html b/image/test/reftest/downscaling/downscale-8px.html new file mode 100644 index 0000000000..c0586a3657 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-8px.html @@ -0,0 +1,27 @@ + + + +Image reftest wrapper + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-moz-icon-1-ref.html b/image/test/reftest/downscaling/downscale-moz-icon-1-ref.html new file mode 100644 index 0000000000..a7dd5ab8aa --- /dev/null +++ b/image/test/reftest/downscaling/downscale-moz-icon-1-ref.html @@ -0,0 +1,41 @@ + + + + + + Reference for downscaling moz-icon images (bug 1261964) + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-moz-icon-1.html b/image/test/reftest/downscaling/downscale-moz-icon-1.html new file mode 100644 index 0000000000..ba37951274 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-moz-icon-1.html @@ -0,0 +1,19 @@ + + + + + + Testcase for downscaling moz-icon images (bug 1261964) + + + + + + diff --git a/image/test/reftest/downscaling/downscale-orient-ref.html b/image/test/reftest/downscaling/downscale-orient-ref.html new file mode 100644 index 0000000000..e32141868f --- /dev/null +++ b/image/test/reftest/downscaling/downscale-orient-ref.html @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-orient-ref.png b/image/test/reftest/downscaling/downscale-orient-ref.png new file mode 100644 index 0000000000..0db684e0d7 Binary files /dev/null and b/image/test/reftest/downscaling/downscale-orient-ref.png differ diff --git a/image/test/reftest/downscaling/downscale-orient.html b/image/test/reftest/downscaling/downscale-orient.html new file mode 100644 index 0000000000..11b849b27e --- /dev/null +++ b/image/test/reftest/downscaling/downscale-orient.html @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-png.html b/image/test/reftest/downscaling/downscale-png.html new file mode 100644 index 0000000000..4752b2155a --- /dev/null +++ b/image/test/reftest/downscaling/downscale-png.html @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-svg-1-ref.html b/image/test/reftest/downscaling/downscale-svg-1-ref.html new file mode 100644 index 0000000000..8935619ebd --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1-ref.html @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/image/test/reftest/downscaling/downscale-svg-1a.html b/image/test/reftest/downscaling/downscale-svg-1a.html new file mode 100644 index 0000000000..2263cc9982 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1a.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/downscale-svg-1b.html b/image/test/reftest/downscaling/downscale-svg-1b.html new file mode 100644 index 0000000000..9db239c7cd --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1b.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/downscale-svg-1c.html b/image/test/reftest/downscaling/downscale-svg-1c.html new file mode 100644 index 0000000000..f8babf0267 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1c.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/downscale-svg-1d.html b/image/test/reftest/downscaling/downscale-svg-1d.html new file mode 100644 index 0000000000..9a56a51de0 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1d.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/downscale-svg-1e.html b/image/test/reftest/downscaling/downscale-svg-1e.html new file mode 100644 index 0000000000..732ac22c96 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1e.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/downscale-svg-1f.html b/image/test/reftest/downscaling/downscale-svg-1f.html new file mode 100644 index 0000000000..0124682c70 --- /dev/null +++ b/image/test/reftest/downscaling/downscale-svg-1f.html @@ -0,0 +1,8 @@ + + + + +
    + + diff --git a/image/test/reftest/downscaling/ff-0RGB.ico b/image/test/reftest/downscaling/ff-0RGB.ico new file mode 100644 index 0000000000..56116b9f6a Binary files /dev/null and b/image/test/reftest/downscaling/ff-0RGB.ico differ diff --git a/image/test/reftest/downscaling/ff-0RGB.png b/image/test/reftest/downscaling/ff-0RGB.png new file mode 100644 index 0000000000..749ffcdfb3 Binary files /dev/null and b/image/test/reftest/downscaling/ff-0RGB.png differ diff --git a/image/test/reftest/downscaling/ff-ARGB.ico b/image/test/reftest/downscaling/ff-ARGB.ico new file mode 100644 index 0000000000..4681dc6495 Binary files /dev/null and b/image/test/reftest/downscaling/ff-ARGB.ico differ diff --git a/image/test/reftest/downscaling/ff-ARGB.png b/image/test/reftest/downscaling/ff-ARGB.png new file mode 100644 index 0000000000..74ea0e2f39 Binary files /dev/null and b/image/test/reftest/downscaling/ff-ARGB.png differ diff --git a/image/test/reftest/downscaling/huge-1.html b/image/test/reftest/downscaling/huge-1.html new file mode 100644 index 0000000000..6685600086 --- /dev/null +++ b/image/test/reftest/downscaling/huge-1.html @@ -0,0 +1,9 @@ + + + + + diff --git a/image/test/reftest/downscaling/image-pre-rotated-90-deg.jpg b/image/test/reftest/downscaling/image-pre-rotated-90-deg.jpg new file mode 100644 index 0000000000..b8817f9bf5 Binary files /dev/null and b/image/test/reftest/downscaling/image-pre-rotated-90-deg.jpg differ diff --git a/image/test/reftest/downscaling/lime-red-256px-bmp-in.ico b/image/test/reftest/downscaling/lime-red-256px-bmp-in.ico new file mode 100644 index 0000000000..b372cba4a2 Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px-bmp-in.ico differ diff --git a/image/test/reftest/downscaling/lime-red-256px-png-in.ico b/image/test/reftest/downscaling/lime-red-256px-png-in.ico new file mode 100644 index 0000000000..e8578d2934 Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px-png-in.ico differ diff --git a/image/test/reftest/downscaling/lime-red-256px.bmp b/image/test/reftest/downscaling/lime-red-256px.bmp new file mode 100644 index 0000000000..3dc808970f Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px.bmp differ diff --git a/image/test/reftest/downscaling/lime-red-256px.gif b/image/test/reftest/downscaling/lime-red-256px.gif new file mode 100644 index 0000000000..f9f669aa4d Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px.gif differ diff --git a/image/test/reftest/downscaling/lime-red-256px.jpg b/image/test/reftest/downscaling/lime-red-256px.jpg new file mode 100644 index 0000000000..ac8efdf360 Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px.jpg differ diff --git a/image/test/reftest/downscaling/lime-red-256px.png b/image/test/reftest/downscaling/lime-red-256px.png new file mode 100644 index 0000000000..2be2e05a59 Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-256px.png differ diff --git a/image/test/reftest/downscaling/lime-red-256px.svg b/image/test/reftest/downscaling/lime-red-256px.svg new file mode 100644 index 0000000000..530ae6d6d5 --- /dev/null +++ b/image/test/reftest/downscaling/lime-red-256px.svg @@ -0,0 +1,5 @@ + + + + diff --git a/image/test/reftest/downscaling/lime-red-32px.png b/image/test/reftest/downscaling/lime-red-32px.png new file mode 100644 index 0000000000..bfa2e7b737 Binary files /dev/null and b/image/test/reftest/downscaling/lime-red-32px.png differ diff --git a/image/test/reftest/downscaling/png-interlaced.png b/image/test/reftest/downscaling/png-interlaced.png new file mode 100644 index 0000000000..a938d0bd6a Binary files /dev/null and b/image/test/reftest/downscaling/png-interlaced.png differ diff --git a/image/test/reftest/downscaling/png-normal.png b/image/test/reftest/downscaling/png-normal.png new file mode 100644 index 0000000000..c2780fdd93 Binary files /dev/null and b/image/test/reftest/downscaling/png-normal.png differ diff --git a/image/test/reftest/downscaling/reftest.list b/image/test/reftest/downscaling/reftest.list new file mode 100644 index 0000000000..f01780ffaa --- /dev/null +++ b/image/test/reftest/downscaling/reftest.list @@ -0,0 +1,217 @@ +# Reftests for downscaling +# +# Downscaling can be a lossy process, so a bit of mismatch is acceptable here, +# as long as it's barely noticeable visually. When necessary, this can be +# explicitly allowed via 'fuzzy'/'fuzzy-if' annotations. +# +# Many of these tests check primarily that we don't lose rows or columns of +# pixels when downscaling by making sure that the result isn't too similar to +# about:blank. A small amount of fuzziness is used to ensure that the tests +# don't pass because of very slight deviations; passing tests should be +# substantially different from about:blank. This fuzziness should *not* be +# removed as doing so would make the tests pass in situations where they +# shouldn't. +# +# IMPORTANT: For robustness, each test should be listed *twice* in this +# manifest -- once with the high quality downscaling pref disabled, and once +# with this pref enabled. The pref is set via "defaults", so +# simply appending a new test to the lists below each of those lines should be +# sufficient. +# +# Also note that Mac OS X has its own system-level downscaling algorithm, so +# tests here may need Mac-specific "fuzzy-if(cocoaWidget,...)" annotations. +# Similarly, modern versions of Windows have slightly different downscaling +# behavior than other platforms, and may require "fuzzy-if(winWidget,...)". + + +# RUN TESTS NOT AFFECTED BY DOWNSCALE-DURING-DECODE: +# ================================================== +fuzzy(0-14,0-416) == downscale-svg-1a.html downscale-svg-1-ref.html?80 +fuzzy(65-65,468-480) == downscale-svg-1b.html downscale-svg-1-ref.html?72 +fuzzy(0-8,0-292) == downscale-svg-1c.html downscale-svg-1-ref.html?64 +fuzzy(7-7,208-208) == downscale-svg-1d.html downscale-svg-1-ref.html?53 +fuzzy(54-55,178-178) == downscale-svg-1e.html downscale-svg-1-ref.html?40 +fuzzy(64-64,31-31) == downscale-svg-1f.html downscale-svg-1-ref.html?24 + +# RUN TESTS WITH DOWNSCALE-DURING-DECODE DISABLED: +# ================================================ +defaults pref(image.downscale-during-decode.enabled,false) + +fuzzy-if(winWidget,0-16,0-20) fuzzy-if(cocoaWidget,0-106,0-31) == downscale-1.html downscale-1-ref.html + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,left about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,left about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,right about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,right about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,top about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,top about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,bottom about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,bottom about:blank + +# Skip on Android because it runs reftests via http, and moz-icon isn't +# accessible from http/https origins anymore. +fuzzy(0-27,0-3940) fuzzy-if(gtkWidget,0-0,0-0) skip-if(Android) fuzzy-if(appleSilicon,0-20,0-10123) == downscale-moz-icon-1.html downscale-moz-icon-1-ref.html # gtkWidget Bug 1592059: regular is 2616, no-accel is 0, qr passes with 0 + +== downscale-png.html?16,16,interlaced downscale-png.html?16,16,normal +== downscale-png.html?24,24,interlaced downscale-png.html?24,24,normal + +# Non-transparent and transparent ICO images +== downscale-16px.html?ff-0RGB.ico downscale-16px.html?ff-0RGB.png +fuzzy(0-1,0-1) fuzzy-if(gtkWidget&&swgl,1-1,1-1) == downscale-16px.html?ff-ARGB.ico downscale-16px.html?ff-ARGB.png + +# Test downscaling from all supported formats from 256 to 32. +== downscale-32px.html?.bmp downscale-32px-ref.html +== downscale-32px.html?.gif downscale-32px-ref.html +fuzzy(0-1,0-1024) == downscale-32px.html?.jpg downscale-32px-ref.html +== downscale-32px.html?.png downscale-32px-ref.html +== downscale-32px.html?.svg downscale-32px-ref.html +== downscale-32px.html?-bmp-in.ico downscale-32px-ref.html +== downscale-32px.html?-png-in.ico downscale-32px-ref.html + +# Test downscaling a JPEG with orientation metadata. +fuzzy(0-1,0-50) == downscale-orient.html downscale-orient-ref.html + +# RUN TESTS WITH DOWNSCALE-DURING-DECODE ENABLED: +# =============================================== +defaults pref(image.downscale-during-decode.enabled,true) + +fuzzy(0-31,0-127) fuzzy-if(d2d,0-31,0-147) == downscale-1.html downscale-1-ref.html # intermittently 147 pixels on win7 accelerated only (not win8) + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,left about:blank +fuzzy(0-20,0-999) != downscale-2f.html?203,52,left about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,left about:blank +fuzzy(0-20,0-999) != downscale-2f.html?205,53,left about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,right about:blank +fuzzy(0-20,0-999) != downscale-2f.html?203,52,right about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,right about:blank +fuzzy(0-20,0-999) != downscale-2f.html?205,53,right about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,top about:blank +fuzzy(0-20,0-999) != downscale-2f.html?203,52,top about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,top about:blank +fuzzy(0-20,0-999) != downscale-2f.html?205,53,top about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2b.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2c.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2d.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2e.html?203,52,bottom about:blank +fuzzy(0-20,0-999) != downscale-2f.html?203,52,bottom about:blank + +fuzzy(0-20,0-999) != downscale-2a.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2b.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2c.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2d.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2e.html?205,53,bottom about:blank +fuzzy(0-20,0-999) != downscale-2f.html?205,53,bottom about:blank + +# Skip on Android because it runs reftests via http, and moz-icon isn't +# accessible from http/https origins anymore. +fuzzy(0-53,0-6391) fuzzy-if(appleSilicon,0-20,0-11605) fuzzy-if(gtkWidget,18-19,5502-5568) skip-if(Android) == downscale-moz-icon-1.html downscale-moz-icon-1-ref.html # gtkWidget Bug 1592059 + +== downscale-png.html?16,16,interlaced downscale-png.html?16,16,normal +== downscale-png.html?24,24,interlaced downscale-png.html?24,24,normal + +# Non-transparent and transparent ICO images +fuzzy(0-1,0-3) == downscale-16px.html?ff-0RGB.ico downscale-16px.html?ff-0RGB.png +fuzzy(0-3,0-32) fuzzy-if(swgl,3-3,33-33) == downscale-16px.html?ff-ARGB.ico downscale-16px.html?ff-ARGB.png + +# Upside-down (negative height) BMP +== downscale-8px.html?top-to-bottom-16x16-24bpp.bmp downscale-8px.html?bmp-size-16x16-24bpp.png + +# Test downscaling from all supported formats from 256 to 32. +fuzzy(0-18,0-128) == downscale-32px.html?.bmp downscale-32px-ref.html +fuzzy(0-18,0-128) == downscale-32px.html?.gif downscale-32px-ref.html +fuzzy(0-19,0-992) == downscale-32px.html?.jpg downscale-32px-ref.html +fuzzy(0-18,0-128) == downscale-32px.html?.png downscale-32px-ref.html +== downscale-32px.html?.svg downscale-32px-ref.html +fuzzy(0-18,0-128) == downscale-32px.html?-bmp-in.ico downscale-32px-ref.html +fuzzy(0-18,0-128) == downscale-32px.html?-png-in.ico downscale-32px-ref.html + +# Test downscaling a JPEG with orientation metadata. +fuzzy(0-4,0-18) == downscale-orient.html downscale-orient-ref.html + +# Test images taller or wider than 32767 pixels. +== huge-1.html?100x32768.png,100,100 huge-1.html?100x100.png,100,100 +== huge-1.html?100x32768.png,100,32768 huge-1.html?100x100.png,100,32768 +== huge-1.html?32768x100.png,100,100 huge-1.html?100x100.png,100,100 +== huge-1.html?32768x100.png,32768,100 huge-1.html?100x100.png,32768,100 +== huge-1.html?100x32768.gif,100,100 huge-1.html?100x100.gif,100,100 +== huge-1.html?100x32768.gif,100,32768 huge-1.html?100x100.gif,100,32768 +== huge-1.html?32768x100.gif,100,100 huge-1.html?100x100.gif,100,100 +== huge-1.html?32768x100.gif,32768,100 huge-1.html?100x100.gif,32768,100 +== huge-1.html?100x32768.jpg,100,100 huge-1.html?100x100.jpg,100,100 +== huge-1.html?100x32768.jpg,100,32768 huge-1.html?100x100.jpg,100,32768 +== huge-1.html?32768x100.jpg,100,100 huge-1.html?100x100.jpg,100,100 +== huge-1.html?32768x100.jpg,32768,100 huge-1.html?100x100.jpg,32768,100 + +# Only need to run these with downscaling on +!= 1421191-1.html about:blank +== 1404366-1.html about:blank diff --git a/image/test/reftest/downscaling/top-to-bottom-16x16-24bpp.bmp b/image/test/reftest/downscaling/top-to-bottom-16x16-24bpp.bmp new file mode 100644 index 0000000000..bd18f85d48 Binary files /dev/null and b/image/test/reftest/downscaling/top-to-bottom-16x16-24bpp.bmp differ diff --git a/image/test/reftest/encoders-lossless/ImageDocument.css b/image/test/reftest/encoders-lossless/ImageDocument.css new file mode 100644 index 0000000000..9a41b4c161 --- /dev/null +++ b/image/test/reftest/encoders-lossless/ImageDocument.css @@ -0,0 +1,16 @@ +body { + background: #222 url("chrome://global/skin/media/imagedoc-darknoise.png"); + margin: 0; +} + +img { + display: block; + position: absolute; + margin: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png"); + color: #222; +} diff --git a/image/test/reftest/encoders-lossless/encoder.html b/image/test/reftest/encoders-lossless/encoder.html new file mode 100644 index 0000000000..6e07995ae3 --- /dev/null +++ b/image/test/reftest/encoders-lossless/encoder.html @@ -0,0 +1,113 @@ + + + Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/encoders-lossless/reftest.list b/image/test/reftest/encoders-lossless/reftest.list new file mode 100644 index 0000000000..fe8fbc5bc5 --- /dev/null +++ b/image/test/reftest/encoders-lossless/reftest.list @@ -0,0 +1,175 @@ +# Encoder ref tests +# These reftests must be run as HTTP because of canvas' origin-clean security +# file:// URLs are always considered from a different origin unless same URL +# +# The test will copy a PNG image to a canvas, then use canvas.toDataUrl to get +# the data, then set the data to a new image hence invoking the appropriate +# encoder. +# +# The tests should only be used with lossless encoders. +# +# Valid arguments for encoder.html in the query string: +# - img= +# - mime= +# - options= +# Example: +# encoder.html?img=escape(reference_image.png) +# &mime=escape(image/vnd.microsoft.icon) +# &options=escape(-moz-parse-options:bpp=24;format=png) + +# PNG +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/png +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/png +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/png +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/png +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/png +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/png +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/png +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/png +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/png +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/png +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/png +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/png +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/png +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/png +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/png + +# BMP using default parse options +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/bmp +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/bmp +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/bmp +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/bmp +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/bmp +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/bmp +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/bmp +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/bmp +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/bmp +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/bmp +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/bmp +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/bmp +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/bmp +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/bmp +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/bmp + +# BMP using image/bmp mime type and 32bpp parse options +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D32 + +# BMP using image/bmp mime type and 24bpp parse options +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/bmp&options=-moz-parse-options%3Abpp%3D24 + +# ICO using default parse options +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/vnd.microsoft.icon +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/vnd.microsoft.icon +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/vnd.microsoft.icon +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/vnd.microsoft.icon +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/vnd.microsoft.icon +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/vnd.microsoft.icon +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/vnd.microsoft.icon +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/vnd.microsoft.icon +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/vnd.microsoft.icon +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/vnd.microsoft.icon +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/vnd.microsoft.icon +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/vnd.microsoft.icon +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/vnd.microsoft.icon +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/vnd.microsoft.icon +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/vnd.microsoft.icon +HTTP == size-256x256.png encoder.html?img=size-256x256.png&mime=image/vnd.microsoft.icon + +# ICO using image/vnd.microsoft.icon mime type and 32bpp parse options with bmp +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp +HTTP == size-256x256.png encoder.html?img=size-256x256.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D32%3Bformat%3Dbmp + +# ICO using image/vnd.microsoft.icon mime type and 24bpp parse options with bmp +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp +HTTP == size-256x256.png encoder.html?img=size-256x256.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Abpp%3D24%3Bformat%3Dbmp + +# ICO using image/vnd.microsoft.icon mime type png +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng +HTTP == size-256x256.png encoder.html?img=size-256x256.png&mime=image/vnd.microsoft.icon&options=-moz-parse-options%3Aformat%3Dpng + +# WEBP +HTTP == size-1x1.png encoder.html?img=size-1x1.png&mime=image/webp&options=1 +HTTP == size-2x2.png encoder.html?img=size-2x2.png&mime=image/webp&options=1 +HTTP == size-3x3.png encoder.html?img=size-3x3.png&mime=image/webp&options=1 +HTTP == size-4x4.png encoder.html?img=size-4x4.png&mime=image/webp&options=1 +HTTP == size-5x5.png encoder.html?img=size-5x5.png&mime=image/webp&options=1 +HTTP == size-6x6.png encoder.html?img=size-6x6.png&mime=image/webp&options=1 +HTTP == size-7x7.png encoder.html?img=size-7x7.png&mime=image/webp&options=1 +HTTP == size-8x8.png encoder.html?img=size-8x8.png&mime=image/webp&options=1 +HTTP == size-9x9.png encoder.html?img=size-9x9.png&mime=image/webp&options=1 +HTTP == size-15x15.png encoder.html?img=size-15x15.png&mime=image/webp&options=1 +HTTP == size-16x16.png encoder.html?img=size-16x16.png&mime=image/webp&options=1 +HTTP == size-17x17.png encoder.html?img=size-17x17.png&mime=image/webp&options=1 +HTTP == size-31x31.png encoder.html?img=size-31x31.png&mime=image/webp&options=1 +HTTP == size-32x32.png encoder.html?img=size-32x32.png&mime=image/webp&options=1 +HTTP == size-33x33.png encoder.html?img=size-33x33.png&mime=image/webp&options=1 diff --git a/image/test/reftest/encoders-lossless/size-15x15.png b/image/test/reftest/encoders-lossless/size-15x15.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-15x15.png differ diff --git a/image/test/reftest/encoders-lossless/size-16x16.png b/image/test/reftest/encoders-lossless/size-16x16.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-16x16.png differ diff --git a/image/test/reftest/encoders-lossless/size-17x17.png b/image/test/reftest/encoders-lossless/size-17x17.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-17x17.png differ diff --git a/image/test/reftest/encoders-lossless/size-1x1.png b/image/test/reftest/encoders-lossless/size-1x1.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-1x1.png differ diff --git a/image/test/reftest/encoders-lossless/size-256x256.png b/image/test/reftest/encoders-lossless/size-256x256.png new file mode 100644 index 0000000000..84bfada761 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-256x256.png differ diff --git a/image/test/reftest/encoders-lossless/size-2x2.png b/image/test/reftest/encoders-lossless/size-2x2.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-2x2.png differ diff --git a/image/test/reftest/encoders-lossless/size-31x31.png b/image/test/reftest/encoders-lossless/size-31x31.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-31x31.png differ diff --git a/image/test/reftest/encoders-lossless/size-32x32.png b/image/test/reftest/encoders-lossless/size-32x32.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-32x32.png differ diff --git a/image/test/reftest/encoders-lossless/size-33x33.png b/image/test/reftest/encoders-lossless/size-33x33.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-33x33.png differ diff --git a/image/test/reftest/encoders-lossless/size-3x3.png b/image/test/reftest/encoders-lossless/size-3x3.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-3x3.png differ diff --git a/image/test/reftest/encoders-lossless/size-4x4.png b/image/test/reftest/encoders-lossless/size-4x4.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-4x4.png differ diff --git a/image/test/reftest/encoders-lossless/size-5x5.png b/image/test/reftest/encoders-lossless/size-5x5.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-5x5.png differ diff --git a/image/test/reftest/encoders-lossless/size-6x6.png b/image/test/reftest/encoders-lossless/size-6x6.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-6x6.png differ diff --git a/image/test/reftest/encoders-lossless/size-7x7.png b/image/test/reftest/encoders-lossless/size-7x7.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-7x7.png differ diff --git a/image/test/reftest/encoders-lossless/size-8x8.png b/image/test/reftest/encoders-lossless/size-8x8.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-8x8.png differ diff --git a/image/test/reftest/encoders-lossless/size-9x9.png b/image/test/reftest/encoders-lossless/size-9x9.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/encoders-lossless/size-9x9.png differ diff --git a/image/test/reftest/encoders-lossless/test.png b/image/test/reftest/encoders-lossless/test.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/encoders-lossless/test.png differ diff --git a/image/test/reftest/generic/accept-image-catchall-ref.html b/image/test/reftest/generic/accept-image-catchall-ref.html new file mode 100644 index 0000000000..32daa9f5d6 --- /dev/null +++ b/image/test/reftest/generic/accept-image-catchall-ref.html @@ -0,0 +1,12 @@ + + + + + Accept: header should include image/* catchall + + + + + diff --git a/image/test/reftest/generic/accept-image-catchall.html b/image/test/reftest/generic/accept-image-catchall.html new file mode 100644 index 0000000000..bcacd9fd8f --- /dev/null +++ b/image/test/reftest/generic/accept-image-catchall.html @@ -0,0 +1,13 @@ + + + + + Accept: header should include */* catchall + + + + + diff --git a/image/test/reftest/generic/check-header.sjs b/image/test/reftest/generic/check-header.sjs new file mode 100644 index 0000000000..afc39cb550 --- /dev/null +++ b/image/test/reftest/generic/check-header.sjs @@ -0,0 +1,72 @@ +const BinaryOutputStream = Components.Constructor("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream", "setOutputStream"); + +function isCatchall(v) +{ + // "*/*" exactly + return /^\*\/\*$/.test(v); +} + +/* +# Python used to generate the following byte array +def toHex(n): + if n < 16: return "0x" + hex(n)[2:].upper() + return "0x" + hex(n)[2:].upper() + +def hexFile(name): + f = open(name, "rb") + try: + while True: + print toHex(ord(f.read(1))) + ", ", + except: + pass + +hexFile("image/test/reftest/generic/green.png") +*/ + +const IMAGE_DATA = + [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, + 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, + 0x00, 0x64, 0x08, 0x02, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x02, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, + 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x9E, 0x49, 0x44, 0x41, 0x54, 0x78, + 0xDA, 0xED, 0xD0, 0x31, 0x01, 0x00, 0x00, 0x08, 0x03, 0xA0, 0x69, + 0xFF, 0xCE, 0x5A, 0xC1, 0xCF, 0x07, 0x22, 0x50, 0x99, 0x70, 0xD4, + 0x0A, 0x64, 0xC9, 0x92, 0x25, 0x4B, 0x96, 0x2C, 0x05, 0xB2, 0x64, + 0xC9, 0x92, 0x25, 0x4B, 0x96, 0x02, 0x59, 0xB2, 0x64, 0xC9, 0x92, + 0x25, 0x4B, 0x81, 0x2C, 0x59, 0xB2, 0x64, 0xC9, 0x92, 0xA5, 0x40, + 0x96, 0x2C, 0x59, 0xB2, 0x64, 0xC9, 0x52, 0x20, 0x4B, 0x96, 0x2C, + 0x59, 0xB2, 0x64, 0x29, 0x90, 0x25, 0x4B, 0x96, 0x2C, 0x59, 0xB2, + 0x14, 0xC8, 0x92, 0x25, 0x4B, 0x96, 0x2C, 0x59, 0x0A, 0x64, 0xC9, + 0x92, 0x25, 0x4B, 0x96, 0x2C, 0x05, 0xB2, 0x64, 0xC9, 0x92, 0x25, + 0x4B, 0x96, 0x02, 0x59, 0xB2, 0x64, 0xC9, 0x92, 0x25, 0x4B, 0x81, + 0x2C, 0x59, 0xB2, 0x64, 0xC9, 0x92, 0xA5, 0x40, 0x96, 0x2C, 0x59, + 0xB2, 0x64, 0xC9, 0x52, 0x20, 0x4B, 0x96, 0x2C, 0x59, 0xB2, 0x64, + 0x29, 0x90, 0x25, 0x4B, 0x96, 0x2C, 0x59, 0xB2, 0x14, 0xC8, 0x92, + 0x25, 0x4B, 0x96, 0x2C, 0x59, 0x0A, 0x64, 0xC9, 0xFA, 0xB6, 0x89, + 0x5F, 0x01, 0xC7, 0x24, 0x83, 0xB2, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + ]; + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + + var accept = request.hasHeader("Accept") + ? request.getHeader("Accept") + : ""; + + if (accept.split(",").some(isCatchall)) + { + response.setHeader("Content-Type", "image/png", false); + + var stream = new BinaryOutputStream(response.bodyOutputStream); + stream.writeByteArray(IMAGE_DATA); + } + else + { + response.setStatusLine(request.httpVersion, 404, "Not found"); + response.write("Accept header contained: " + accept); + } +} diff --git a/image/test/reftest/generic/green.png b/image/test/reftest/generic/green.png new file mode 100644 index 0000000000..4718c00e62 Binary files /dev/null and b/image/test/reftest/generic/green.png differ diff --git a/image/test/reftest/generic/moz-icon-1.html b/image/test/reftest/generic/moz-icon-1.html new file mode 100644 index 0000000000..fb2dcea202 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-1.html @@ -0,0 +1 @@ + diff --git a/image/test/reftest/generic/moz-icon-blank-1-almostref.html b/image/test/reftest/generic/moz-icon-blank-1-almostref.html new file mode 100644 index 0000000000..38d5e1ec86 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-blank-1-almostref.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/reftest/generic/moz-icon-blank-1-antiref.html b/image/test/reftest/generic/moz-icon-blank-1-antiref.html new file mode 100644 index 0000000000..2631ff4018 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-blank-1-antiref.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/reftest/generic/moz-icon-blank-1-antiref2.html b/image/test/reftest/generic/moz-icon-blank-1-antiref2.html new file mode 100644 index 0000000000..8553c81be0 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-blank-1-antiref2.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/reftest/generic/moz-icon-blank-1-ref.html b/image/test/reftest/generic/moz-icon-blank-1-ref.html new file mode 100644 index 0000000000..086d5af837 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-blank-1-ref.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/reftest/generic/moz-icon-blank-1.html b/image/test/reftest/generic/moz-icon-blank-1.html new file mode 100644 index 0000000000..2b52c96297 --- /dev/null +++ b/image/test/reftest/generic/moz-icon-blank-1.html @@ -0,0 +1,2 @@ + + diff --git a/image/test/reftest/generic/reftest.list b/image/test/reftest/generic/reftest.list new file mode 100644 index 0000000000..6f236399ef --- /dev/null +++ b/image/test/reftest/generic/reftest.list @@ -0,0 +1,6 @@ +HTTP == accept-image-catchall.html accept-image-catchall-ref.html +skip-if(Android) != moz-icon-1.html about:blank +== moz-icon-blank-1.html moz-icon-blank-1-ref.html +skip-if(Android) != moz-icon-blank-1-ref.html moz-icon-blank-1-antiref.html +skip-if(Android) != moz-icon-blank-1-ref.html moz-icon-blank-1-antiref2.html +fuzzy-if(OSX,44-49,335-348) fuzzy-if(winWidget,64-140,45-191) == moz-icon-blank-1-almostref.html moz-icon-blank-1-ref.html diff --git a/image/test/reftest/gif/1bit-255-trans.gif b/image/test/reftest/gif/1bit-255-trans.gif new file mode 100644 index 0000000000..60273ba81d Binary files /dev/null and b/image/test/reftest/gif/1bit-255-trans.gif differ diff --git a/image/test/reftest/gif/1bit-255-trans.png b/image/test/reftest/gif/1bit-255-trans.png new file mode 100644 index 0000000000..611480ac4c Binary files /dev/null and b/image/test/reftest/gif/1bit-255-trans.png differ diff --git a/image/test/reftest/gif/ImageDocument.css b/image/test/reftest/gif/ImageDocument.css new file mode 100644 index 0000000000..b449810986 --- /dev/null +++ b/image/test/reftest/gif/ImageDocument.css @@ -0,0 +1,16 @@ +body { + background-image: url("chrome://global/skin/media/imagedoc-darknoise.png"); + margin: 0; +} + +body > :first-child { + display: block; + position: absolute; + margin: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png"); + color: #222; +} diff --git a/image/test/reftest/gif/animation1a.gif b/image/test/reftest/gif/animation1a.gif new file mode 100644 index 0000000000..d328276545 Binary files /dev/null and b/image/test/reftest/gif/animation1a.gif differ diff --git a/image/test/reftest/gif/animation2a-finalframe.gif b/image/test/reftest/gif/animation2a-finalframe.gif new file mode 100644 index 0000000000..8d9c4aaf7d Binary files /dev/null and b/image/test/reftest/gif/animation2a-finalframe.gif differ diff --git a/image/test/reftest/gif/animation2a.gif b/image/test/reftest/gif/animation2a.gif new file mode 100644 index 0000000000..07abf8d986 Binary files /dev/null and b/image/test/reftest/gif/animation2a.gif differ diff --git a/image/test/reftest/gif/blue.gif b/image/test/reftest/gif/blue.gif new file mode 100644 index 0000000000..f9dbeeea8e Binary files /dev/null and b/image/test/reftest/gif/blue.gif differ diff --git a/image/test/reftest/gif/comment.gif b/image/test/reftest/gif/comment.gif new file mode 100644 index 0000000000..255cceb418 Binary files /dev/null and b/image/test/reftest/gif/comment.gif differ diff --git a/image/test/reftest/gif/comment.png b/image/test/reftest/gif/comment.png new file mode 100644 index 0000000000..89394ba186 Binary files /dev/null and b/image/test/reftest/gif/comment.png differ diff --git a/image/test/reftest/gif/delaytest.html b/image/test/reftest/gif/delaytest.html new file mode 100644 index 0000000000..265a201ab5 --- /dev/null +++ b/image/test/reftest/gif/delaytest.html @@ -0,0 +1,58 @@ + + + +Delayed image reftest wrapper + + + + + + + diff --git a/image/test/reftest/gif/in-colormap-trans.gif b/image/test/reftest/gif/in-colormap-trans.gif new file mode 100644 index 0000000000..48f5c7caf1 Binary files /dev/null and b/image/test/reftest/gif/in-colormap-trans.gif differ diff --git a/image/test/reftest/gif/in-colormap-trans.png b/image/test/reftest/gif/in-colormap-trans.png new file mode 100644 index 0000000000..08761dfe41 Binary files /dev/null and b/image/test/reftest/gif/in-colormap-trans.png differ diff --git a/image/test/reftest/gif/one-color-offset-ref.gif b/image/test/reftest/gif/one-color-offset-ref.gif new file mode 100644 index 0000000000..14a59ff477 Binary files /dev/null and b/image/test/reftest/gif/one-color-offset-ref.gif differ diff --git a/image/test/reftest/gif/one-color-offset.gif b/image/test/reftest/gif/one-color-offset.gif new file mode 100644 index 0000000000..e6d7c49322 Binary files /dev/null and b/image/test/reftest/gif/one-color-offset.gif differ diff --git a/image/test/reftest/gif/one-pixel-no-image-data-ref.html b/image/test/reftest/gif/one-pixel-no-image-data-ref.html new file mode 100644 index 0000000000..b23c11dc1b --- /dev/null +++ b/image/test/reftest/gif/one-pixel-no-image-data-ref.html @@ -0,0 +1,11 @@ + + +
    diff --git a/image/test/reftest/gif/one-pixel-no-image-data.html b/image/test/reftest/gif/one-pixel-no-image-data.html new file mode 100644 index 0000000000..e09e90bd8b --- /dev/null +++ b/image/test/reftest/gif/one-pixel-no-image-data.html @@ -0,0 +1,11 @@ + + +
    diff --git a/image/test/reftest/gif/out-of-colormap-trans.gif b/image/test/reftest/gif/out-of-colormap-trans.gif new file mode 100644 index 0000000000..17e747c9b2 Binary files /dev/null and b/image/test/reftest/gif/out-of-colormap-trans.gif differ diff --git a/image/test/reftest/gif/out-of-colormap-trans.png b/image/test/reftest/gif/out-of-colormap-trans.png new file mode 100644 index 0000000000..8d3eb581a2 Binary files /dev/null and b/image/test/reftest/gif/out-of-colormap-trans.png differ diff --git a/image/test/reftest/gif/red.gif b/image/test/reftest/gif/red.gif new file mode 100644 index 0000000000..d3c32bae25 Binary files /dev/null and b/image/test/reftest/gif/red.gif differ diff --git a/image/test/reftest/gif/reftest.list b/image/test/reftest/gif/reftest.list new file mode 100644 index 0000000000..1e44a83931 --- /dev/null +++ b/image/test/reftest/gif/reftest.list @@ -0,0 +1,32 @@ +# GIF tests + +# tests for bug 519589 +== 1bit-255-trans.gif 1bit-255-trans.png +== in-colormap-trans.gif in-colormap-trans.png +== out-of-colormap-trans.gif out-of-colormap-trans.png + +# a GIF file that uses the comment extension +== comment.gif comment.png + +# a GIF file with a background smaller than the size of the canvas +== small-background-size.gif small-background-size-ref.gif +== small-background-size-2.gif small-background-size-2-ref.gif + +# a transparent gif that disposes previous frames with clear; we must properly +# clear each frame to pass. +== delaytest.html?transparent-animation.gif transparent-animation-finalframe.html + +# test for bug 641198 +skip == test_bug641198.html animation2a-finalframe.gif # Disabled; see bug 1120144. + +# Bug 1062886: a gif with a single color and an offset +== one-color-offset.gif one-color-offset-ref.gif + +# Bug 1068230 +fuzzy-if(Android,0-1,0-8) == tile-transform.html tile-transform-ref.html + +# Bug 1234077 +== truncated-framerect.html truncated-framerect-ref.html + +# Bug 1801397 +== one-pixel-no-image-data.html one-pixel-no-image-data-ref.html diff --git a/image/test/reftest/gif/small-background-size-2-ref.gif b/image/test/reftest/gif/small-background-size-2-ref.gif new file mode 100644 index 0000000000..b513c41aa2 Binary files /dev/null and b/image/test/reftest/gif/small-background-size-2-ref.gif differ diff --git a/image/test/reftest/gif/small-background-size-2.gif b/image/test/reftest/gif/small-background-size-2.gif new file mode 100644 index 0000000000..a5e2147678 Binary files /dev/null and b/image/test/reftest/gif/small-background-size-2.gif differ diff --git a/image/test/reftest/gif/small-background-size-ref.gif b/image/test/reftest/gif/small-background-size-ref.gif new file mode 100644 index 0000000000..1b656ce439 Binary files /dev/null and b/image/test/reftest/gif/small-background-size-ref.gif differ diff --git a/image/test/reftest/gif/small-background-size.gif b/image/test/reftest/gif/small-background-size.gif new file mode 100644 index 0000000000..8185eb71e8 Binary files /dev/null and b/image/test/reftest/gif/small-background-size.gif differ diff --git a/image/test/reftest/gif/test_bug641198.html b/image/test/reftest/gif/test_bug641198.html new file mode 100644 index 0000000000..46bdb0d47c --- /dev/null +++ b/image/test/reftest/gif/test_bug641198.html @@ -0,0 +1,53 @@ + + + +Test for bug 641198 + + + + + +Animated + + + + + diff --git a/image/test/reftest/gif/tile-transform-ref.html b/image/test/reftest/gif/tile-transform-ref.html new file mode 100644 index 0000000000..5dac1a5bdf --- /dev/null +++ b/image/test/reftest/gif/tile-transform-ref.html @@ -0,0 +1,12 @@ + + + + + Intermediate surface should be transformed correctly when tiling an image + + + + + diff --git a/image/test/reftest/gif/tile-transform.html b/image/test/reftest/gif/tile-transform.html new file mode 100644 index 0000000000..541ae6bbc4 --- /dev/null +++ b/image/test/reftest/gif/tile-transform.html @@ -0,0 +1,12 @@ + + + + + Intermediate surface should be transformed correctly when tiling an image + + + + + diff --git a/image/test/reftest/gif/tiletest-ref.png b/image/test/reftest/gif/tiletest-ref.png new file mode 100644 index 0000000000..b493899cc8 Binary files /dev/null and b/image/test/reftest/gif/tiletest-ref.png differ diff --git a/image/test/reftest/gif/tiletest.gif b/image/test/reftest/gif/tiletest.gif new file mode 100644 index 0000000000..7a04c9654a Binary files /dev/null and b/image/test/reftest/gif/tiletest.gif differ diff --git a/image/test/reftest/gif/transparent-animation-finalframe.gif b/image/test/reftest/gif/transparent-animation-finalframe.gif new file mode 100644 index 0000000000..a55f92a813 Binary files /dev/null and b/image/test/reftest/gif/transparent-animation-finalframe.gif differ diff --git a/image/test/reftest/gif/transparent-animation-finalframe.html b/image/test/reftest/gif/transparent-animation-finalframe.html new file mode 100644 index 0000000000..6f160d1b7c --- /dev/null +++ b/image/test/reftest/gif/transparent-animation-finalframe.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/image/test/reftest/gif/transparent-animation.gif b/image/test/reftest/gif/transparent-animation.gif new file mode 100644 index 0000000000..b2895487bd Binary files /dev/null and b/image/test/reftest/gif/transparent-animation.gif differ diff --git a/image/test/reftest/gif/truncated-framerect-interlaced-ref.gif b/image/test/reftest/gif/truncated-framerect-interlaced-ref.gif new file mode 100644 index 0000000000..ca9bf2fa71 Binary files /dev/null and b/image/test/reftest/gif/truncated-framerect-interlaced-ref.gif differ diff --git a/image/test/reftest/gif/truncated-framerect-interlaced.gif b/image/test/reftest/gif/truncated-framerect-interlaced.gif new file mode 100644 index 0000000000..59709898bd Binary files /dev/null and b/image/test/reftest/gif/truncated-framerect-interlaced.gif differ diff --git a/image/test/reftest/gif/truncated-framerect-ref.gif b/image/test/reftest/gif/truncated-framerect-ref.gif new file mode 100644 index 0000000000..ab79a455b0 Binary files /dev/null and b/image/test/reftest/gif/truncated-framerect-ref.gif differ diff --git a/image/test/reftest/gif/truncated-framerect-ref.html b/image/test/reftest/gif/truncated-framerect-ref.html new file mode 100644 index 0000000000..ef48b8a192 --- /dev/null +++ b/image/test/reftest/gif/truncated-framerect-ref.html @@ -0,0 +1,33 @@ + + + + + Bug 1234077 - Make sure GIFs still render correctly with a truncated frameRect + + + + + +
    + +
    + +
    + +
    + + diff --git a/image/test/reftest/gif/truncated-framerect.gif b/image/test/reftest/gif/truncated-framerect.gif new file mode 100644 index 0000000000..8febb2a748 Binary files /dev/null and b/image/test/reftest/gif/truncated-framerect.gif differ diff --git a/image/test/reftest/gif/truncated-framerect.html b/image/test/reftest/gif/truncated-framerect.html new file mode 100644 index 0000000000..c1c5df6538 --- /dev/null +++ b/image/test/reftest/gif/truncated-framerect.html @@ -0,0 +1,28 @@ + + + + + Bug 1234077 - Make sure GIFs still render correctly with a truncated frameRect + + + + + +
    + +
    + +
    + +
    + + diff --git a/image/test/reftest/ico/cur/pointer.cur b/image/test/reftest/ico/cur/pointer.cur new file mode 100644 index 0000000000..025ebaed1f Binary files /dev/null and b/image/test/reftest/ico/cur/pointer.cur differ diff --git a/image/test/reftest/ico/cur/pointer.png b/image/test/reftest/ico/cur/pointer.png new file mode 100644 index 0000000000..84ad8f3fbd Binary files /dev/null and b/image/test/reftest/ico/cur/pointer.png differ diff --git a/image/test/reftest/ico/cur/reftest.list b/image/test/reftest/ico/cur/reftest.list new file mode 100644 index 0000000000..635136506b --- /dev/null +++ b/image/test/reftest/ico/cur/reftest.list @@ -0,0 +1,4 @@ +# ICO BMP and PNG mixed tests + +== wrapper.html?pointer.cur wrapper.html?pointer.png + diff --git a/image/test/reftest/ico/cur/wrapper.html b/image/test/reftest/ico/cur/wrapper.html new file mode 100644 index 0000000000..45b5167754 --- /dev/null +++ b/image/test/reftest/ico/cur/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.ico new file mode 100644 index 0000000000..0f51d504af Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.png new file mode 100644 index 0000000000..152b30d716 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-not-square-transparent-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.ico new file mode 100644 index 0000000000..def2a4ece7 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.png new file mode 100644 index 0000000000..064a68bb2e Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-partial-transparent-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.ico new file mode 100644 index 0000000000..b68cf0ef0d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.png new file mode 100644 index 0000000000..956c78ece6 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-15x15-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.ico new file mode 100644 index 0000000000..d96a4a0e1d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.png new file mode 100644 index 0000000000..90088351fa Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-16x16-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.ico new file mode 100644 index 0000000000..4f10ad13c0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.png new file mode 100644 index 0000000000..9a294696c4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-17x17-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.ico new file mode 100644 index 0000000000..5af8bef61a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.png new file mode 100644 index 0000000000..7a07a124ea Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-1x1-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.ico new file mode 100644 index 0000000000..63d95e3b7c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.png new file mode 100644 index 0000000000..0a23d8c8ee Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-256x256-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.ico new file mode 100644 index 0000000000..09c140f1b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.png new file mode 100644 index 0000000000..3b09f80769 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-2x2-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.ico new file mode 100644 index 0000000000..bbfc3165ab Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.png new file mode 100644 index 0000000000..d1fe6ddeee Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-31x31-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.ico new file mode 100644 index 0000000000..279ecb8358 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.png new file mode 100644 index 0000000000..078d3dc5d0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-32x32-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.ico new file mode 100644 index 0000000000..fa1862c1ca Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.png new file mode 100644 index 0000000000..e64e12b2aa Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-33x33-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.ico new file mode 100644 index 0000000000..733b1f12bc Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.png new file mode 100644 index 0000000000..b8519a8749 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-3x3-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.ico new file mode 100644 index 0000000000..ba3097cecd Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.png new file mode 100644 index 0000000000..3977b54548 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-4x4-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.ico new file mode 100644 index 0000000000..52e32df27b Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.png new file mode 100644 index 0000000000..caa9246b67 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-5x5-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.ico new file mode 100644 index 0000000000..c29651400f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.png new file mode 100644 index 0000000000..30e1b0249d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-6x6-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.ico new file mode 100644 index 0000000000..8ce9915c44 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.png new file mode 100644 index 0000000000..9dbaae84cb Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-7x7-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.ico new file mode 100644 index 0000000000..485dff028a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.png new file mode 100644 index 0000000000..2201388408 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-8x8-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.ico new file mode 100644 index 0000000000..38f34ec50a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.png new file mode 100644 index 0000000000..7fe1b548b8 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-size-9x9-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.ico b/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.ico new file mode 100644 index 0000000000..8e361306c5 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.png b/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.png new file mode 100644 index 0000000000..062152e3b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-1bpp/ico-transparent-1bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-1bpp/reftest.list b/image/test/reftest/ico/ico-bmp-1bpp/reftest.list new file mode 100644 index 0000000000..1b9ca1348a --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-1bpp/reftest.list @@ -0,0 +1,23 @@ +# ICO BMP 1BPP tests + +# Images of various sizes +== ico-size-1x1-1bpp.ico ico-size-1x1-1bpp.png +== ico-size-2x2-1bpp.ico ico-size-2x2-1bpp.png +== ico-size-3x3-1bpp.ico ico-size-3x3-1bpp.png +== ico-size-4x4-1bpp.ico ico-size-4x4-1bpp.png +== ico-size-5x5-1bpp.ico ico-size-5x5-1bpp.png +== ico-size-6x6-1bpp.ico ico-size-6x6-1bpp.png +== ico-size-7x7-1bpp.ico ico-size-7x7-1bpp.png +== ico-size-8x8-1bpp.ico ico-size-8x8-1bpp.png +== ico-size-9x9-1bpp.ico ico-size-9x9-1bpp.png +== ico-size-15x15-1bpp.ico ico-size-15x15-1bpp.png +== ico-size-16x16-1bpp.ico ico-size-16x16-1bpp.png +== ico-size-17x17-1bpp.ico ico-size-17x17-1bpp.png +== ico-size-31x31-1bpp.ico ico-size-31x31-1bpp.png +== ico-size-32x32-1bpp.ico ico-size-32x32-1bpp.png +== ico-size-33x33-1bpp.ico ico-size-33x33-1bpp.png +== ico-size-256x256-1bpp.ico ico-size-256x256-1bpp.png +== ico-partial-transparent-1bpp.ico ico-partial-transparent-1bpp.png +== ico-transparent-1bpp.ico ico-transparent-1bpp.png +== ico-not-square-transparent-1bpp.ico ico-not-square-transparent-1bpp.png + diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.ico new file mode 100644 index 0000000000..16d6584ef8 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.png new file mode 100644 index 0000000000..a881048b96 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-not-square-transparent-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.ico new file mode 100644 index 0000000000..ab0dc4bce1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.png new file mode 100644 index 0000000000..0363210c74 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-partial-transparent-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.ico new file mode 100644 index 0000000000..8721b0d169 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-15x15-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.ico new file mode 100644 index 0000000000..04e4736189 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-16x16-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.ico new file mode 100644 index 0000000000..308ccb7a61 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-17x17-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.ico new file mode 100644 index 0000000000..e2bf90c093 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-1x1-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.ico new file mode 100644 index 0000000000..c3977400a4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.png new file mode 100644 index 0000000000..84bfada761 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-256x256-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.ico new file mode 100644 index 0000000000..dba180a07a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-2x2-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.ico new file mode 100644 index 0000000000..aa67502f61 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-31x31-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.ico new file mode 100644 index 0000000000..a85b871c5e Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-32x32-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.ico new file mode 100644 index 0000000000..a5c49374de Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-33x33-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.ico new file mode 100644 index 0000000000..8a0b9433f4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-3x3-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.ico new file mode 100644 index 0000000000..feb3f11e16 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-4x4-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.ico new file mode 100644 index 0000000000..d607ca5724 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-5x5-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.ico new file mode 100644 index 0000000000..62a231602c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-6x6-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.ico new file mode 100644 index 0000000000..d884ecfd77 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-7x7-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.ico new file mode 100644 index 0000000000..782ae220df Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-8x8-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.ico new file mode 100644 index 0000000000..97992643b6 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-size-9x9-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.ico b/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.ico new file mode 100644 index 0000000000..8e361306c5 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.png b/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.png new file mode 100644 index 0000000000..062152e3b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-24bpp/ico-transparent-24bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-24bpp/reftest.list b/image/test/reftest/ico/ico-bmp-24bpp/reftest.list new file mode 100644 index 0000000000..8772936603 --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-24bpp/reftest.list @@ -0,0 +1,23 @@ +# ICO BMP 24BPP tests + +# Images of various sizes +== ico-size-1x1-24bpp.ico ico-size-1x1-24bpp.png +== ico-size-2x2-24bpp.ico ico-size-2x2-24bpp.png +== ico-size-3x3-24bpp.ico ico-size-3x3-24bpp.png +== ico-size-4x4-24bpp.ico ico-size-4x4-24bpp.png +== ico-size-5x5-24bpp.ico ico-size-5x5-24bpp.png +== ico-size-6x6-24bpp.ico ico-size-6x6-24bpp.png +== ico-size-7x7-24bpp.ico ico-size-7x7-24bpp.png +== ico-size-8x8-24bpp.ico ico-size-8x8-24bpp.png +== ico-size-9x9-24bpp.ico ico-size-9x9-24bpp.png +== ico-size-15x15-24bpp.ico ico-size-15x15-24bpp.png +== ico-size-16x16-24bpp.ico ico-size-16x16-24bpp.png +== ico-size-17x17-24bpp.ico ico-size-17x17-24bpp.png +== ico-size-31x31-24bpp.ico ico-size-31x31-24bpp.png +== ico-size-32x32-24bpp.ico ico-size-32x32-24bpp.png +== ico-size-33x33-24bpp.ico ico-size-33x33-24bpp.png +== ico-size-256x256-24bpp.ico ico-size-256x256-24bpp.png +== ico-partial-transparent-24bpp.ico ico-partial-transparent-24bpp.png +== ico-transparent-24bpp.ico ico-transparent-24bpp.png +== ico-not-square-transparent-24bpp.ico ico-not-square-transparent-24bpp.png + diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.ico new file mode 100644 index 0000000000..dd0299c413 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.png new file mode 100644 index 0000000000..befc665552 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-not-square-transparent-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.ico new file mode 100644 index 0000000000..8ad62f7d0e Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.png new file mode 100644 index 0000000000..226ad6494f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-partial-transparent-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.ico new file mode 100644 index 0000000000..1f1b6b51ce Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-15x15-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.ico new file mode 100644 index 0000000000..7a8f01529d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-16x16-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.ico new file mode 100644 index 0000000000..b92860be4f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-17x17-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.ico new file mode 100644 index 0000000000..5ad60c575c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-1x1-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.ico new file mode 100644 index 0000000000..f8b530ef0b Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.png new file mode 100644 index 0000000000..84bfada761 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-256x256-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.ico new file mode 100644 index 0000000000..e5b2bf7e76 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-2x2-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.ico new file mode 100644 index 0000000000..ddcbde85f4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-31x31-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.ico new file mode 100644 index 0000000000..a89c016485 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-32x32-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.ico new file mode 100644 index 0000000000..cda9133f8b Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-33x33-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.ico new file mode 100644 index 0000000000..3894ccf212 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-3x3-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.ico new file mode 100644 index 0000000000..828494c668 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-4x4-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.ico new file mode 100644 index 0000000000..4f0a2bcc72 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-5x5-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.ico new file mode 100644 index 0000000000..5524769e60 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-6x6-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.ico new file mode 100644 index 0000000000..6aeebb898d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-7x7-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.ico new file mode 100644 index 0000000000..824c744a20 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-8x8-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.ico new file mode 100644 index 0000000000..cf1f6e9c88 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-size-9x9-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.ico b/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.ico new file mode 100644 index 0000000000..151b7cb361 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.png b/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.png new file mode 100644 index 0000000000..062152e3b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-32bpp/ico-transparent-32bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-32bpp/reftest.list b/image/test/reftest/ico/ico-bmp-32bpp/reftest.list new file mode 100644 index 0000000000..e05355a2bf --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-32bpp/reftest.list @@ -0,0 +1,22 @@ +# ICO BMP 32BPP tests + +# Images of various sizes +== ico-size-1x1-32bpp.ico ico-size-1x1-32bpp.png +== ico-size-2x2-32bpp.ico ico-size-2x2-32bpp.png +== ico-size-3x3-32bpp.ico ico-size-3x3-32bpp.png +== ico-size-4x4-32bpp.ico ico-size-4x4-32bpp.png +== ico-size-5x5-32bpp.ico ico-size-5x5-32bpp.png +== ico-size-6x6-32bpp.ico ico-size-6x6-32bpp.png +== ico-size-7x7-32bpp.ico ico-size-7x7-32bpp.png +== ico-size-8x8-32bpp.ico ico-size-8x8-32bpp.png +== ico-size-9x9-32bpp.ico ico-size-9x9-32bpp.png +== ico-size-15x15-32bpp.ico ico-size-15x15-32bpp.png +== ico-size-16x16-32bpp.ico ico-size-16x16-32bpp.png +== ico-size-17x17-32bpp.ico ico-size-17x17-32bpp.png +== ico-size-31x31-32bpp.ico ico-size-31x31-32bpp.png +== ico-size-32x32-32bpp.ico ico-size-32x32-32bpp.png +== ico-size-33x33-32bpp.ico ico-size-33x33-32bpp.png +== ico-size-256x256-32bpp.ico ico-size-256x256-32bpp.png +== ico-partial-transparent-32bpp.ico ico-partial-transparent-32bpp.png +== ico-transparent-32bpp.ico ico-transparent-32bpp.png +== ico-not-square-transparent-32bpp.ico ico-not-square-transparent-32bpp.png diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.ico new file mode 100644 index 0000000000..d502d2ef6b Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.png new file mode 100644 index 0000000000..3e556ad293 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-not-square-transparent-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.ico new file mode 100644 index 0000000000..7bd3b8a69f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.png new file mode 100644 index 0000000000..9ff0ce41f7 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-partial-transparent-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.ico new file mode 100644 index 0000000000..de5c49e2a6 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.png new file mode 100644 index 0000000000..5d4a3f9534 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-15x15-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.ico new file mode 100644 index 0000000000..b856b3f376 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.png new file mode 100644 index 0000000000..d45d63f539 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-16x16-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.ico new file mode 100644 index 0000000000..44e055d2f7 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.png new file mode 100644 index 0000000000..bf48903299 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-17x17-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.ico new file mode 100644 index 0000000000..fd46c328d5 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.png new file mode 100644 index 0000000000..d41dd645b7 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-1x1-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.ico new file mode 100644 index 0000000000..6d28edaa8f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.png new file mode 100644 index 0000000000..3acdef8303 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-256x256-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.ico new file mode 100644 index 0000000000..7dc4afde68 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.png new file mode 100644 index 0000000000..b2d6050415 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-2x2-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.ico new file mode 100644 index 0000000000..0471332d6b Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.png new file mode 100644 index 0000000000..cb12a3448d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-31x31-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.ico new file mode 100644 index 0000000000..ef005dc5bf Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.png new file mode 100644 index 0000000000..58d867d120 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-32x32-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.ico new file mode 100644 index 0000000000..4c71963a49 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.png new file mode 100644 index 0000000000..064fde198c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-33x33-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.ico new file mode 100644 index 0000000000..aaa6350e96 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.png new file mode 100644 index 0000000000..e34114d5c9 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-3x3-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.ico new file mode 100644 index 0000000000..767bebed41 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.png new file mode 100644 index 0000000000..3efa555620 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-4x4-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.ico new file mode 100644 index 0000000000..309b6fe5b0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.png new file mode 100644 index 0000000000..02ebf57a51 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-5x5-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.ico new file mode 100644 index 0000000000..255fda6a84 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.png new file mode 100644 index 0000000000..1f5769d09c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-6x6-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.ico new file mode 100644 index 0000000000..1a39634526 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.png new file mode 100644 index 0000000000..59a1b98b52 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-7x7-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.ico new file mode 100644 index 0000000000..40bc9f8937 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.png new file mode 100644 index 0000000000..cf44f59676 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-8x8-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.ico new file mode 100644 index 0000000000..bda12f32b9 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.png new file mode 100644 index 0000000000..2e07364135 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-size-9x9-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.ico b/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.ico new file mode 100644 index 0000000000..8e361306c5 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.png b/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.png new file mode 100644 index 0000000000..062152e3b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-4bpp/ico-transparent-4bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-4bpp/reftest.list b/image/test/reftest/ico/ico-bmp-4bpp/reftest.list new file mode 100644 index 0000000000..6caac4ac86 --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-4bpp/reftest.list @@ -0,0 +1,23 @@ +# ICO BMP 4BPP tests + +# Images of various sizes +== ico-size-1x1-4bpp.ico ico-size-1x1-4bpp.png +== ico-size-2x2-4bpp.ico ico-size-2x2-4bpp.png +== ico-size-3x3-4bpp.ico ico-size-3x3-4bpp.png +== ico-size-4x4-4bpp.ico ico-size-4x4-4bpp.png +== ico-size-5x5-4bpp.ico ico-size-5x5-4bpp.png +== ico-size-6x6-4bpp.ico ico-size-6x6-4bpp.png +== ico-size-7x7-4bpp.ico ico-size-7x7-4bpp.png +== ico-size-8x8-4bpp.ico ico-size-8x8-4bpp.png +== ico-size-9x9-4bpp.ico ico-size-9x9-4bpp.png +== ico-size-15x15-4bpp.ico ico-size-15x15-4bpp.png +== ico-size-16x16-4bpp.ico ico-size-16x16-4bpp.png +== ico-size-17x17-4bpp.ico ico-size-17x17-4bpp.png +== ico-size-31x31-4bpp.ico ico-size-31x31-4bpp.png +== ico-size-32x32-4bpp.ico ico-size-32x32-4bpp.png +== ico-size-33x33-4bpp.ico ico-size-33x33-4bpp.png +== ico-size-256x256-4bpp.ico ico-size-256x256-4bpp.png +== ico-partial-transparent-4bpp.ico ico-partial-transparent-4bpp.png +== ico-transparent-4bpp.ico ico-transparent-4bpp.png +== ico-not-square-transparent-4bpp.ico ico-not-square-transparent-4bpp.png + diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.ico new file mode 100644 index 0000000000..d28b9a04ec Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.png new file mode 100644 index 0000000000..36a4eb5122 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-not-square-transparent-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.ico new file mode 100644 index 0000000000..9074caa401 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.png new file mode 100644 index 0000000000..6f990f2572 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-partial-transparent-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.ico new file mode 100644 index 0000000000..f3f3a13530 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-15x15-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.ico new file mode 100644 index 0000000000..24c20e23ec Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.png new file mode 100644 index 0000000000..2e66b2e5f9 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-16x16-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.ico new file mode 100644 index 0000000000..7fa66b9b21 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.png new file mode 100644 index 0000000000..4d11d7561c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-17x17-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.ico new file mode 100644 index 0000000000..3cf3320eae Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-1x1-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.ico new file mode 100644 index 0000000000..524b6f7c8e Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.png new file mode 100644 index 0000000000..f367468c95 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-256x256-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.ico new file mode 100644 index 0000000000..95d8375a00 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-2x2-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.ico new file mode 100644 index 0000000000..7806754474 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.png new file mode 100644 index 0000000000..84bf61078c Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-31x31-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.ico new file mode 100644 index 0000000000..d21cc5b967 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.png new file mode 100644 index 0000000000..349fd4df24 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-32x32-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.ico new file mode 100644 index 0000000000..1b419b2632 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.png new file mode 100644 index 0000000000..a4c100649a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-33x33-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.ico new file mode 100644 index 0000000000..869f74fcdc Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-3x3-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.ico new file mode 100644 index 0000000000..3967563727 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-4x4-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.ico new file mode 100644 index 0000000000..92814e3661 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-5x5-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.ico new file mode 100644 index 0000000000..1af478a8a8 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-6x6-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.ico new file mode 100644 index 0000000000..1c70820ebd Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-7x7-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.ico new file mode 100644 index 0000000000..782ae220df Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-8x8-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.ico new file mode 100644 index 0000000000..6825372b4a Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-size-9x9-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.ico b/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.ico new file mode 100644 index 0000000000..8e361306c5 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.png b/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.png new file mode 100644 index 0000000000..062152e3b1 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-8bpp/ico-transparent-8bpp.png differ diff --git a/image/test/reftest/ico/ico-bmp-8bpp/reftest.list b/image/test/reftest/ico/ico-bmp-8bpp/reftest.list new file mode 100644 index 0000000000..5a6b543238 --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-8bpp/reftest.list @@ -0,0 +1,23 @@ +# ICO BMP 8BPP tests + +# Images of various sizes +== ico-size-1x1-8bpp.ico ico-size-1x1-8bpp.png +== ico-size-2x2-8bpp.ico ico-size-2x2-8bpp.png +== ico-size-3x3-8bpp.ico ico-size-3x3-8bpp.png +== ico-size-4x4-8bpp.ico ico-size-4x4-8bpp.png +== ico-size-5x5-8bpp.ico ico-size-5x5-8bpp.png +== ico-size-6x6-8bpp.ico ico-size-6x6-8bpp.png +== ico-size-7x7-8bpp.ico ico-size-7x7-8bpp.png +== ico-size-8x8-8bpp.ico ico-size-8x8-8bpp.png +== ico-size-9x9-8bpp.ico ico-size-9x9-8bpp.png +== ico-size-15x15-8bpp.ico ico-size-15x15-8bpp.png +== ico-size-16x16-8bpp.ico ico-size-16x16-8bpp.png +== ico-size-17x17-8bpp.ico ico-size-17x17-8bpp.png +== ico-size-31x31-8bpp.ico ico-size-31x31-8bpp.png +== ico-size-32x32-8bpp.ico ico-size-32x32-8bpp.png +== ico-size-33x33-8bpp.ico ico-size-33x33-8bpp.png +== ico-size-256x256-8bpp.ico ico-size-256x256-8bpp.png +== ico-partial-transparent-8bpp.ico ico-partial-transparent-8bpp.png +== ico-transparent-8bpp.ico ico-transparent-8bpp.png +== ico-not-square-transparent-8bpp.ico ico-not-square-transparent-8bpp.png + diff --git a/image/test/reftest/ico/ico-bmp-corrupted/16x16.png b/image/test/reftest/ico/ico-bmp-corrupted/16x16.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-corrupted/16x16.png differ diff --git a/image/test/reftest/ico/ico-bmp-corrupted/invalid-bpp.ico b/image/test/reftest/ico/ico-bmp-corrupted/invalid-bpp.ico new file mode 100644 index 0000000000..1189e4c040 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-corrupted/invalid-bpp.ico differ diff --git a/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE4.ico b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE4.ico new file mode 100644 index 0000000000..8fd0a5d658 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE4.ico differ diff --git a/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE8.ico b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE8.ico new file mode 100644 index 0000000000..1f185ca620 Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression-RLE8.ico differ diff --git a/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression.ico b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression.ico new file mode 100644 index 0000000000..a49a783c5f Binary files /dev/null and b/image/test/reftest/ico/ico-bmp-corrupted/invalid-compression.ico differ diff --git a/image/test/reftest/ico/ico-bmp-corrupted/reftest.list b/image/test/reftest/ico/ico-bmp-corrupted/reftest.list new file mode 100644 index 0000000000..2467b1323e --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-corrupted/reftest.list @@ -0,0 +1,10 @@ +# ICOs containing corrupted BMP tests + +# Invalid value for bits per pixel (BPP) - detected when decoding the header. +== wrapper.html?invalid-bpp.ico about:blank +# Invalid BPP values for RLE4 - detected when decoding the image data. +== wrapper.html?invalid-compression-RLE4.ico about:blank +# Invalid BPP values for RLE8 - detected when decoding the image data. +== wrapper.html?invalid-compression-RLE8.ico about:blank +# Invalid compression value - detected when decoding the image data. +== wrapper.html?invalid-compression.ico about:blank diff --git a/image/test/reftest/ico/ico-bmp-corrupted/wrapper.html b/image/test/reftest/ico/ico-bmp-corrupted/wrapper.html new file mode 100644 index 0000000000..943bc75bf2 --- /dev/null +++ b/image/test/reftest/ico/ico-bmp-corrupted/wrapper.html @@ -0,0 +1,80 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/ico/ico-mixed/mixed-bmp-png.ico b/image/test/reftest/ico/ico-mixed/mixed-bmp-png.ico new file mode 100644 index 0000000000..32e2c4995c Binary files /dev/null and b/image/test/reftest/ico/ico-mixed/mixed-bmp-png.ico differ diff --git a/image/test/reftest/ico/ico-mixed/mixed-bmp-png.png b/image/test/reftest/ico/ico-mixed/mixed-bmp-png.png new file mode 100644 index 0000000000..b6aee74092 Binary files /dev/null and b/image/test/reftest/ico/ico-mixed/mixed-bmp-png.png differ diff --git a/image/test/reftest/ico/ico-mixed/mixed-bmp-png32.png b/image/test/reftest/ico/ico-mixed/mixed-bmp-png32.png new file mode 100644 index 0000000000..a058991272 Binary files /dev/null and b/image/test/reftest/ico/ico-mixed/mixed-bmp-png32.png differ diff --git a/image/test/reftest/ico/ico-mixed/mixed-bmp-png48.png b/image/test/reftest/ico/ico-mixed/mixed-bmp-png48.png new file mode 100644 index 0000000000..61bea5c804 Binary files /dev/null and b/image/test/reftest/ico/ico-mixed/mixed-bmp-png48.png differ diff --git a/image/test/reftest/ico/ico-mixed/reftest.list b/image/test/reftest/ico/ico-mixed/reftest.list new file mode 100644 index 0000000000..36134e40ab --- /dev/null +++ b/image/test/reftest/ico/ico-mixed/reftest.list @@ -0,0 +1,3 @@ +# ICO BMP and PNG mixed tests + +== mixed-bmp-png.ico mixed-bmp-png48.png diff --git a/image/test/reftest/ico/ico-png/corrupted_x00n0g01.ico b/image/test/reftest/ico/ico-png/corrupted_x00n0g01.ico new file mode 100644 index 0000000000..18b97b0b7e Binary files /dev/null and b/image/test/reftest/ico/ico-png/corrupted_x00n0g01.ico differ diff --git a/image/test/reftest/ico/ico-png/corrupted_xxcrn0g04.ico b/image/test/reftest/ico/ico-png/corrupted_xxcrn0g04.ico new file mode 100644 index 0000000000..3fa5285c5b Binary files /dev/null and b/image/test/reftest/ico/ico-png/corrupted_xxcrn0g04.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-15x15-png.ico b/image/test/reftest/ico/ico-png/ico-size-15x15-png.ico new file mode 100644 index 0000000000..e67644a890 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-15x15-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-15x15-png.png b/image/test/reftest/ico/ico-png/ico-size-15x15-png.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-15x15-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-16x16-png.ico b/image/test/reftest/ico/ico-png/ico-size-16x16-png.ico new file mode 100644 index 0000000000..442ab4dc80 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-16x16-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-16x16-png.png b/image/test/reftest/ico/ico-png/ico-size-16x16-png.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-16x16-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-17x17-png.ico b/image/test/reftest/ico/ico-png/ico-size-17x17-png.ico new file mode 100644 index 0000000000..f135385d7e Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-17x17-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-17x17-png.png b/image/test/reftest/ico/ico-png/ico-size-17x17-png.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-17x17-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-1x1-png.ico b/image/test/reftest/ico/ico-png/ico-size-1x1-png.ico new file mode 100644 index 0000000000..8eb80c7db2 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-1x1-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-1x1-png.png b/image/test/reftest/ico/ico-png/ico-size-1x1-png.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-1x1-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-256x256-png.ico b/image/test/reftest/ico/ico-png/ico-size-256x256-png.ico new file mode 100644 index 0000000000..ecb88edf3c Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-256x256-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-256x256-png.png b/image/test/reftest/ico/ico-png/ico-size-256x256-png.png new file mode 100644 index 0000000000..2d2f52d6c1 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-256x256-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-2x2-png.ico b/image/test/reftest/ico/ico-png/ico-size-2x2-png.ico new file mode 100644 index 0000000000..5799953c98 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-2x2-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-2x2-png.png b/image/test/reftest/ico/ico-png/ico-size-2x2-png.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-2x2-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-31x31-png.ico b/image/test/reftest/ico/ico-png/ico-size-31x31-png.ico new file mode 100644 index 0000000000..2e9fbd8f9c Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-31x31-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-31x31-png.png b/image/test/reftest/ico/ico-png/ico-size-31x31-png.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-31x31-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-32x32-png.ico b/image/test/reftest/ico/ico-png/ico-size-32x32-png.ico new file mode 100644 index 0000000000..af97a86630 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-32x32-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-32x32-png.png b/image/test/reftest/ico/ico-png/ico-size-32x32-png.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-32x32-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-33x33-png.ico b/image/test/reftest/ico/ico-png/ico-size-33x33-png.ico new file mode 100644 index 0000000000..2509c8c1f2 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-33x33-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-33x33-png.png b/image/test/reftest/ico/ico-png/ico-size-33x33-png.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-33x33-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-3x3-png.ico b/image/test/reftest/ico/ico-png/ico-size-3x3-png.ico new file mode 100644 index 0000000000..d2cd649c84 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-3x3-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-3x3-png.png b/image/test/reftest/ico/ico-png/ico-size-3x3-png.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-3x3-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-4x4-png.ico b/image/test/reftest/ico/ico-png/ico-size-4x4-png.ico new file mode 100644 index 0000000000..60180aad5e Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-4x4-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-4x4-png.png b/image/test/reftest/ico/ico-png/ico-size-4x4-png.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-4x4-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-5x5-png.ico b/image/test/reftest/ico/ico-png/ico-size-5x5-png.ico new file mode 100644 index 0000000000..089c0c8858 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-5x5-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-5x5-png.png b/image/test/reftest/ico/ico-png/ico-size-5x5-png.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-5x5-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-6x6-png.ico b/image/test/reftest/ico/ico-png/ico-size-6x6-png.ico new file mode 100644 index 0000000000..2ee75d25ab Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-6x6-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-6x6-png.png b/image/test/reftest/ico/ico-png/ico-size-6x6-png.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-6x6-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-7x7-png.ico b/image/test/reftest/ico/ico-png/ico-size-7x7-png.ico new file mode 100644 index 0000000000..ade9a3ecde Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-7x7-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-7x7-png.png b/image/test/reftest/ico/ico-png/ico-size-7x7-png.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-7x7-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-8x8-png.ico b/image/test/reftest/ico/ico-png/ico-size-8x8-png.ico new file mode 100644 index 0000000000..a0a150bad6 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-8x8-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-8x8-png.png b/image/test/reftest/ico/ico-png/ico-size-8x8-png.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-8x8-png.png differ diff --git a/image/test/reftest/ico/ico-png/ico-size-9x9-png.ico b/image/test/reftest/ico/ico-png/ico-size-9x9-png.ico new file mode 100644 index 0000000000..a53357b449 Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-9x9-png.ico differ diff --git a/image/test/reftest/ico/ico-png/ico-size-9x9-png.png b/image/test/reftest/ico/ico-png/ico-size-9x9-png.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/ico/ico-png/ico-size-9x9-png.png differ diff --git a/image/test/reftest/ico/ico-png/reftest.list b/image/test/reftest/ico/ico-png/reftest.list new file mode 100644 index 0000000000..002d0e4f32 --- /dev/null +++ b/image/test/reftest/ico/ico-png/reftest.list @@ -0,0 +1,29 @@ +# ICO PNG tests + +# Images of various sizes +== ico-size-1x1-png.ico ico-size-1x1-png.png +== ico-size-2x2-png.ico ico-size-2x2-png.png +== ico-size-3x3-png.ico ico-size-3x3-png.png +== ico-size-4x4-png.ico ico-size-4x4-png.png +== ico-size-5x5-png.ico ico-size-5x5-png.png +== ico-size-6x6-png.ico ico-size-6x6-png.png +== ico-size-7x7-png.ico ico-size-7x7-png.png +== ico-size-8x8-png.ico ico-size-8x8-png.png +== ico-size-9x9-png.ico ico-size-9x9-png.png +== ico-size-15x15-png.ico ico-size-15x15-png.png +== ico-size-16x16-png.ico ico-size-16x16-png.png +== ico-size-17x17-png.ico ico-size-17x17-png.png +== ico-size-31x31-png.ico ico-size-31x31-png.png +== ico-size-32x32-png.ico ico-size-32x32-png.png +== ico-size-33x33-png.ico ico-size-33x33-png.png +== ico-size-256x256-png.ico ico-size-256x256-png.png + +# Corrupted files so no image should be loaded +# x00n0g01 - empty 0x0 grayscale file +== wrapper.html?x00n0g01.ico about:blank +# xcrn0g04 - added cr bytes +== wrapper.html?xcrn0g04.ico about:blank + +# Test ICO PNG transparency +== transparent-png.ico transparent-png.png + diff --git a/image/test/reftest/ico/ico-png/tmp.ico b/image/test/reftest/ico/ico-png/tmp.ico new file mode 100644 index 0000000000..5723a2e776 Binary files /dev/null and b/image/test/reftest/ico/ico-png/tmp.ico differ diff --git a/image/test/reftest/ico/ico-png/transparent-png.ico b/image/test/reftest/ico/ico-png/transparent-png.ico new file mode 100644 index 0000000000..cc8a4a31db Binary files /dev/null and b/image/test/reftest/ico/ico-png/transparent-png.ico differ diff --git a/image/test/reftest/ico/ico-png/transparent-png.png b/image/test/reftest/ico/ico-png/transparent-png.png new file mode 100644 index 0000000000..29e3a24359 Binary files /dev/null and b/image/test/reftest/ico/ico-png/transparent-png.png differ diff --git a/image/test/reftest/ico/ico-png/wrapper.html b/image/test/reftest/ico/ico-png/wrapper.html new file mode 100644 index 0000000000..45b5167754 --- /dev/null +++ b/image/test/reftest/ico/ico-png/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/ico/ico-png/x00n0g01.png b/image/test/reftest/ico/ico-png/x00n0g01.png new file mode 100644 index 0000000000..db3a5fda7e Binary files /dev/null and b/image/test/reftest/ico/ico-png/x00n0g01.png differ diff --git a/image/test/reftest/ico/ico-png/xcrn0g04.png b/image/test/reftest/ico/ico-png/xcrn0g04.png new file mode 100644 index 0000000000..5bce9f3ada Binary files /dev/null and b/image/test/reftest/ico/ico-png/xcrn0g04.png differ diff --git a/image/test/reftest/ico/reftest.list b/image/test/reftest/ico/reftest.list new file mode 100644 index 0000000000..22ed9b4fe7 --- /dev/null +++ b/image/test/reftest/ico/reftest.list @@ -0,0 +1,11 @@ +# ICO tests + +include ico-bmp-1bpp/reftest.list +include ico-bmp-4bpp/reftest.list +include ico-bmp-8bpp/reftest.list +include ico-bmp-24bpp/reftest.list +include ico-bmp-32bpp/reftest.list +include ico-bmp-corrupted/reftest.list +include ico-png/reftest.list +include ico-mixed/reftest.list +include cur/reftest.list diff --git a/image/test/reftest/img2html.html b/image/test/reftest/img2html.html new file mode 100644 index 0000000000..57f45bbdd3 --- /dev/null +++ b/image/test/reftest/img2html.html @@ -0,0 +1,122 @@ + + +Image-to-html converter + + + +

    Image-to-html converter

    +

    Enter the relative path to an image file, and this will convert it +to a pure HTML representation (no images).

    + + +
    + Path to image:
    + + Fill canvas with (instead of transparency).
    + +

    +
    + (img / canvas/ imghtml) +

    + Result:
    + +
    + + + + + + diff --git a/image/test/reftest/jpeg/blue.html b/image/test/reftest/jpeg/blue.html new file mode 100644 index 0000000000..3f4bb44f7e --- /dev/null +++ b/image/test/reftest/jpeg/blue.html @@ -0,0 +1 @@ + diff --git a/image/test/reftest/jpeg/blue.jpg b/image/test/reftest/jpeg/blue.jpg new file mode 100644 index 0000000000..b5fef5d26c Binary files /dev/null and b/image/test/reftest/jpeg/blue.jpg differ diff --git a/image/test/reftest/jpeg/jpg-cmyk-1.jpg b/image/test/reftest/jpeg/jpg-cmyk-1.jpg new file mode 100644 index 0000000000..ddb2c106f2 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-cmyk-1.jpg differ diff --git a/image/test/reftest/jpeg/jpg-cmyk-1.png b/image/test/reftest/jpeg/jpg-cmyk-1.png new file mode 100644 index 0000000000..06915d5bce Binary files /dev/null and b/image/test/reftest/jpeg/jpg-cmyk-1.png differ diff --git a/image/test/reftest/jpeg/jpg-cmyk-2.jpg b/image/test/reftest/jpeg/jpg-cmyk-2.jpg new file mode 100644 index 0000000000..b955bde549 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-cmyk-2.jpg differ diff --git a/image/test/reftest/jpeg/jpg-cmyk-2.png b/image/test/reftest/jpeg/jpg-cmyk-2.png new file mode 100644 index 0000000000..9691e42b65 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-cmyk-2.png differ diff --git a/image/test/reftest/jpeg/jpg-gray.jpg b/image/test/reftest/jpeg/jpg-gray.jpg new file mode 100644 index 0000000000..af0413e3c1 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-gray.jpg differ diff --git a/image/test/reftest/jpeg/jpg-gray.png b/image/test/reftest/jpeg/jpg-gray.png new file mode 100644 index 0000000000..c5aedc34dc Binary files /dev/null and b/image/test/reftest/jpeg/jpg-gray.png differ diff --git a/image/test/reftest/jpeg/jpg-progressive-1000-ref.html b/image/test/reftest/jpeg/jpg-progressive-1000-ref.html new file mode 100644 index 0000000000..0ff2497102 --- /dev/null +++ b/image/test/reftest/jpeg/jpg-progressive-1000-ref.html @@ -0,0 +1 @@ + diff --git a/image/test/reftest/jpeg/jpg-progressive-1000.html b/image/test/reftest/jpeg/jpg-progressive-1000.html new file mode 100644 index 0000000000..b9eaf9c53a --- /dev/null +++ b/image/test/reftest/jpeg/jpg-progressive-1000.html @@ -0,0 +1 @@ + diff --git a/image/test/reftest/jpeg/jpg-progressive-1000.jpg b/image/test/reftest/jpeg/jpg-progressive-1000.jpg new file mode 100644 index 0000000000..65a4325b53 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-progressive-1000.jpg differ diff --git a/image/test/reftest/jpeg/jpg-progressive.jpg b/image/test/reftest/jpeg/jpg-progressive.jpg new file mode 100644 index 0000000000..db3cf59c26 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-progressive.jpg differ diff --git a/image/test/reftest/jpeg/jpg-progressive.png b/image/test/reftest/jpeg/jpg-progressive.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-progressive.png differ diff --git a/image/test/reftest/jpeg/jpg-size-15x15.jpg b/image/test/reftest/jpeg/jpg-size-15x15.jpg new file mode 100644 index 0000000000..efe120a27f Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-15x15.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-15x15.png b/image/test/reftest/jpeg/jpg-size-15x15.png new file mode 100644 index 0000000000..e1287430d0 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-15x15.png differ diff --git a/image/test/reftest/jpeg/jpg-size-16x16.jpg b/image/test/reftest/jpeg/jpg-size-16x16.jpg new file mode 100644 index 0000000000..148ec733f9 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-16x16.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-16x16.png b/image/test/reftest/jpeg/jpg-size-16x16.png new file mode 100644 index 0000000000..c04869e728 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-16x16.png differ diff --git a/image/test/reftest/jpeg/jpg-size-17x17.jpg b/image/test/reftest/jpeg/jpg-size-17x17.jpg new file mode 100644 index 0000000000..b06bdb0d69 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-17x17.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-17x17.png b/image/test/reftest/jpeg/jpg-size-17x17.png new file mode 100644 index 0000000000..00fb8e4f37 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-17x17.png differ diff --git a/image/test/reftest/jpeg/jpg-size-1x1.jpg b/image/test/reftest/jpeg/jpg-size-1x1.jpg new file mode 100644 index 0000000000..73b68dfc06 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-1x1.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-1x1.png b/image/test/reftest/jpeg/jpg-size-1x1.png new file mode 100644 index 0000000000..c05f5fef89 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-1x1.png differ diff --git a/image/test/reftest/jpeg/jpg-size-2x2.jpg b/image/test/reftest/jpeg/jpg-size-2x2.jpg new file mode 100644 index 0000000000..bc50260ea6 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-2x2.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-2x2.png b/image/test/reftest/jpeg/jpg-size-2x2.png new file mode 100644 index 0000000000..e512d3f9b4 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-2x2.png differ diff --git a/image/test/reftest/jpeg/jpg-size-31x31.jpg b/image/test/reftest/jpeg/jpg-size-31x31.jpg new file mode 100644 index 0000000000..8fa0cc2367 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-31x31.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-31x31.png b/image/test/reftest/jpeg/jpg-size-31x31.png new file mode 100644 index 0000000000..e4a8642514 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-31x31.png differ diff --git a/image/test/reftest/jpeg/jpg-size-32x32.jpg b/image/test/reftest/jpeg/jpg-size-32x32.jpg new file mode 100644 index 0000000000..b11d62df6d Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-32x32.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-32x32.png b/image/test/reftest/jpeg/jpg-size-32x32.png new file mode 100644 index 0000000000..3a6fbe8ee9 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-32x32.png differ diff --git a/image/test/reftest/jpeg/jpg-size-33x33.jpg b/image/test/reftest/jpeg/jpg-size-33x33.jpg new file mode 100644 index 0000000000..5ac1169b4b Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-33x33.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-33x33.png b/image/test/reftest/jpeg/jpg-size-33x33.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-33x33.png differ diff --git a/image/test/reftest/jpeg/jpg-size-3x3.jpg b/image/test/reftest/jpeg/jpg-size-3x3.jpg new file mode 100644 index 0000000000..cf370d8ece Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-3x3.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-3x3.png b/image/test/reftest/jpeg/jpg-size-3x3.png new file mode 100644 index 0000000000..cb42ec4f87 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-3x3.png differ diff --git a/image/test/reftest/jpeg/jpg-size-4x4.jpg b/image/test/reftest/jpeg/jpg-size-4x4.jpg new file mode 100644 index 0000000000..5adf760a1b Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-4x4.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-4x4.png b/image/test/reftest/jpeg/jpg-size-4x4.png new file mode 100644 index 0000000000..e6afafd89a Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-4x4.png differ diff --git a/image/test/reftest/jpeg/jpg-size-5x5.jpg b/image/test/reftest/jpeg/jpg-size-5x5.jpg new file mode 100644 index 0000000000..4d5fd0501c Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-5x5.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-5x5.png b/image/test/reftest/jpeg/jpg-size-5x5.png new file mode 100644 index 0000000000..a844aff76d Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-5x5.png differ diff --git a/image/test/reftest/jpeg/jpg-size-6x6.jpg b/image/test/reftest/jpeg/jpg-size-6x6.jpg new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-6x6.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-6x6.png b/image/test/reftest/jpeg/jpg-size-6x6.png new file mode 100644 index 0000000000..415c2d9c6a Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-6x6.png differ diff --git a/image/test/reftest/jpeg/jpg-size-7x7.jpg b/image/test/reftest/jpeg/jpg-size-7x7.jpg new file mode 100644 index 0000000000..5495f7e43e Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-7x7.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-7x7.png b/image/test/reftest/jpeg/jpg-size-7x7.png new file mode 100644 index 0000000000..ab2f892747 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-7x7.png differ diff --git a/image/test/reftest/jpeg/jpg-size-8x8.jpg b/image/test/reftest/jpeg/jpg-size-8x8.jpg new file mode 100644 index 0000000000..84a5c8f426 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-8x8.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-8x8.png b/image/test/reftest/jpeg/jpg-size-8x8.png new file mode 100644 index 0000000000..fe2ff40a1d Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-8x8.png differ diff --git a/image/test/reftest/jpeg/jpg-size-9x9.jpg b/image/test/reftest/jpeg/jpg-size-9x9.jpg new file mode 100644 index 0000000000..d0a15e599f Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-9x9.jpg differ diff --git a/image/test/reftest/jpeg/jpg-size-9x9.png b/image/test/reftest/jpeg/jpg-size-9x9.png new file mode 100644 index 0000000000..18ab4b25de Binary files /dev/null and b/image/test/reftest/jpeg/jpg-size-9x9.png differ diff --git a/image/test/reftest/jpeg/jpg-srgb-icc.jpg b/image/test/reftest/jpeg/jpg-srgb-icc.jpg new file mode 100644 index 0000000000..3ebefac732 Binary files /dev/null and b/image/test/reftest/jpeg/jpg-srgb-icc.jpg differ diff --git a/image/test/reftest/jpeg/jpg-srgb-icc.png b/image/test/reftest/jpeg/jpg-srgb-icc.png new file mode 100644 index 0000000000..1d8efc687a Binary files /dev/null and b/image/test/reftest/jpeg/jpg-srgb-icc.png differ diff --git a/image/test/reftest/jpeg/non-interleaved_progressive-1-halfred-ref.png b/image/test/reftest/jpeg/non-interleaved_progressive-1-halfred-ref.png new file mode 100644 index 0000000000..7d0c9e5580 Binary files /dev/null and b/image/test/reftest/jpeg/non-interleaved_progressive-1-halfred-ref.png differ diff --git a/image/test/reftest/jpeg/non-interleaved_progressive-1.jpg b/image/test/reftest/jpeg/non-interleaved_progressive-1.jpg new file mode 100644 index 0000000000..5128473dfd Binary files /dev/null and b/image/test/reftest/jpeg/non-interleaved_progressive-1.jpg differ diff --git a/image/test/reftest/jpeg/non-interleaved_progressive-2-white-ref.png b/image/test/reftest/jpeg/non-interleaved_progressive-2-white-ref.png new file mode 100644 index 0000000000..597329a663 Binary files /dev/null and b/image/test/reftest/jpeg/non-interleaved_progressive-2-white-ref.png differ diff --git a/image/test/reftest/jpeg/non-interleaved_progressive-2.jpg b/image/test/reftest/jpeg/non-interleaved_progressive-2.jpg new file mode 100644 index 0000000000..d4c80e9124 Binary files /dev/null and b/image/test/reftest/jpeg/non-interleaved_progressive-2.jpg differ diff --git a/image/test/reftest/jpeg/red-bad-marker.jpg b/image/test/reftest/jpeg/red-bad-marker.jpg new file mode 100644 index 0000000000..4abda17e59 Binary files /dev/null and b/image/test/reftest/jpeg/red-bad-marker.jpg differ diff --git a/image/test/reftest/jpeg/red.jpg b/image/test/reftest/jpeg/red.jpg new file mode 100644 index 0000000000..8fca4b938f Binary files /dev/null and b/image/test/reftest/jpeg/red.jpg differ diff --git a/image/test/reftest/jpeg/reftest.list b/image/test/reftest/jpeg/reftest.list new file mode 100644 index 0000000000..d8723b8071 --- /dev/null +++ b/image/test/reftest/jpeg/reftest.list @@ -0,0 +1,73 @@ +# JPEG tests + +# Images of various sizes. +== jpg-size-1x1.jpg jpg-size-1x1.png +== jpg-size-2x2.jpg jpg-size-2x2.png +== jpg-size-3x3.jpg jpg-size-3x3.png +== jpg-size-4x4.jpg jpg-size-4x4.png +== jpg-size-5x5.jpg jpg-size-5x5.png +== jpg-size-6x6.jpg jpg-size-6x6.png +== jpg-size-7x7.jpg jpg-size-7x7.png +== jpg-size-8x8.jpg jpg-size-8x8.png +== jpg-size-9x9.jpg jpg-size-9x9.png +== jpg-size-15x15.jpg jpg-size-15x15.png +== jpg-size-16x16.jpg jpg-size-16x16.png +== jpg-size-17x17.jpg jpg-size-17x17.png +== jpg-size-31x31.jpg jpg-size-31x31.png +== jpg-size-32x32.jpg jpg-size-32x32.png +== jpg-size-33x33.jpg jpg-size-33x33.png +# Progressive encoding +== jpg-progressive.jpg jpg-progressive.png +# Grayscale colorspace +== jpg-gray.jpg jpg-gray.png +# CMYK colorspace +== jpg-cmyk-1.jpg jpg-cmyk-1.png +== jpg-cmyk-2.jpg jpg-cmyk-2.png +# This intermittently fails on Android due to async image decoding (bug #685516) +# Sometimes the image decodes in time and the test passes, other times the image +# appears blank and the test fails. This only seems to be triggered since the +# switch to 24-bit colour (bug #803299). +random-if(Android) == jpg-srgb-icc.jpg jpg-srgb-icc.png + +# webcam-simulacrum.mjpg is a hand-edited file containing red.jpg and blue.jpg, +# concatenated together with the relevant headers for +# multipart/x-mixed-replace. Specifically, with the headers in +# webcam-simulacrum.mjpg^headers^, the web browser will get the following: +# +# HTTP 200 OK +# Content-Type: multipart/x-mixed-replace;boundary=BOUNDARYOMG +# +# --BOUNDARYOMG\r\n +# Content-Type: image/jpeg\r\n +# \r\n +# (no newline) +# --BOUNDARYOMG\r\n +# Content-Type: image/jpeg\r\n +# \r\n +# (no newline) +# --BOUNDARYOMG--\r\n +# +# (The boundary is arbitrary, and just has to be defined as something that +# won't be in the text of the contents themselves. --$(boundary)\r\n means +# "Here is the beginning of a boundary," and --$(boundary)-- means "All done +# sending you parts.") +HTTP == webcam-simulacrum.mjpg blue.jpg +# Same as the above but as img elements in html files to get better test +# coverage of multipart images. +# Images loaded at the top level are actually loaded into an ImageDocument +# which is a slim html document wrapper around the image. +# Multipart images send multiple OnStartRequest's, each OnStartRequest causes +# us to create a new ImageDocument to show the next part. +# This differs from displaying a multipart image in an img element in a regular +# document where we use the same document and same elements and same layout +# frames for each part. So we have this testcase to test those different +# codepaths. +HTTP == webcam-simulacrum.html blue.html + +== non-interleaved_progressive-1.jpg non-interleaved_progressive-1-halfred-ref.png +== non-interleaved_progressive-2.jpg non-interleaved_progressive-2-white-ref.png + +== red-bad-marker.jpg red.jpg + +# check that we reject jpegs with > 1000 scans +== jpg-progressive-1000.html jpg-progressive-1000-ref.html diff --git a/image/test/reftest/jpeg/webcam-simulacrum.html b/image/test/reftest/jpeg/webcam-simulacrum.html new file mode 100644 index 0000000000..2fc6395c7a --- /dev/null +++ b/image/test/reftest/jpeg/webcam-simulacrum.html @@ -0,0 +1 @@ + diff --git a/image/test/reftest/jpeg/webcam-simulacrum.mjpg b/image/test/reftest/jpeg/webcam-simulacrum.mjpg new file mode 100644 index 0000000000..a593273c0e Binary files /dev/null and b/image/test/reftest/jpeg/webcam-simulacrum.mjpg differ diff --git a/image/test/reftest/jpeg/webcam-simulacrum.mjpg^headers^ b/image/test/reftest/jpeg/webcam-simulacrum.mjpg^headers^ new file mode 100644 index 0000000000..f5e846508e --- /dev/null +++ b/image/test/reftest/jpeg/webcam-simulacrum.mjpg^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Content-Type: multipart/x-mixed-replace;boundary=BOUNDARYOMG +Cache-Control: no-cache diff --git a/image/test/reftest/jxl/jxl-size-33x33.jxl b/image/test/reftest/jxl/jxl-size-33x33.jxl new file mode 100644 index 0000000000..da7c1c76f1 Binary files /dev/null and b/image/test/reftest/jxl/jxl-size-33x33.jxl differ diff --git a/image/test/reftest/jxl/jxl-size-33x33.png b/image/test/reftest/jxl/jxl-size-33x33.png new file mode 100644 index 0000000000..72ef7eb636 Binary files /dev/null and b/image/test/reftest/jxl/jxl-size-33x33.png differ diff --git a/image/test/reftest/jxl/reftest.list b/image/test/reftest/jxl/reftest.list new file mode 100644 index 0000000000..04819b5543 --- /dev/null +++ b/image/test/reftest/jxl/reftest.list @@ -0,0 +1,3 @@ +# JXL tests + +pref(image.jxl.enabled,true) == jxl-size-33x33.jxl jxl-size-33x33.png diff --git a/image/test/reftest/pngsuite-ancillary/ccwn2c08.html b/image/test/reftest/pngsuite-ancillary/ccwn2c08.html new file mode 100644 index 0000000000..dc4996e2b0 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ccwn2c08.html @@ -0,0 +1,1242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ccwn2c08.png b/image/test/reftest/pngsuite-ancillary/ccwn2c08.png new file mode 100644 index 0000000000..47c24817b7 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ccwn2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ccwn3p08.html b/image/test/reftest/pngsuite-ancillary/ccwn3p08.html new file mode 100644 index 0000000000..52e636eaad --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ccwn3p08.html @@ -0,0 +1,1272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ccwn3p08.png b/image/test/reftest/pngsuite-ancillary/ccwn3p08.png new file mode 100644 index 0000000000..8bb2c10981 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ccwn3p08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cdfn2c08.html b/image/test/reftest/pngsuite-ancillary/cdfn2c08.html new file mode 100644 index 0000000000..aaae670ec4 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cdfn2c08.html @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cdfn2c08.png b/image/test/reftest/pngsuite-ancillary/cdfn2c08.png new file mode 100644 index 0000000000..559e5261e7 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cdfn2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cdhn2c08.html b/image/test/reftest/pngsuite-ancillary/cdhn2c08.html new file mode 100644 index 0000000000..d56ebf2e1b --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cdhn2c08.html @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cdhn2c08.png b/image/test/reftest/pngsuite-ancillary/cdhn2c08.png new file mode 100644 index 0000000000..3e07e8ecbd Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cdhn2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cdsn2c08.html b/image/test/reftest/pngsuite-ancillary/cdsn2c08.html new file mode 100644 index 0000000000..3ba83a6f5a --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cdsn2c08.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cdsn2c08.png b/image/test/reftest/pngsuite-ancillary/cdsn2c08.png new file mode 100644 index 0000000000..076c32cc08 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cdsn2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cdun2c08.html b/image/test/reftest/pngsuite-ancillary/cdun2c08.html new file mode 100644 index 0000000000..b782337185 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cdun2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cdun2c08.png b/image/test/reftest/pngsuite-ancillary/cdun2c08.png new file mode 100644 index 0000000000..846033be6b Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cdun2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ch1n3p04.html b/image/test/reftest/pngsuite-ancillary/ch1n3p04.html new file mode 100644 index 0000000000..dc2a121de2 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ch1n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ch1n3p04.png b/image/test/reftest/pngsuite-ancillary/ch1n3p04.png new file mode 100644 index 0000000000..17cd12dfc9 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ch1n3p04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ch2n3p08.html b/image/test/reftest/pngsuite-ancillary/ch2n3p08.html new file mode 100644 index 0000000000..78b72c61c6 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ch2n3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ch2n3p08.png b/image/test/reftest/pngsuite-ancillary/ch2n3p08.png new file mode 100644 index 0000000000..25c17987a7 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ch2n3p08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cm0n0g04.html b/image/test/reftest/pngsuite-ancillary/cm0n0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cm0n0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cm0n0g04.png b/image/test/reftest/pngsuite-ancillary/cm0n0g04.png new file mode 100644 index 0000000000..9fba5db3b8 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cm0n0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cm7n0g04.html b/image/test/reftest/pngsuite-ancillary/cm7n0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cm7n0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cm7n0g04.png b/image/test/reftest/pngsuite-ancillary/cm7n0g04.png new file mode 100644 index 0000000000..f7dc46e685 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cm7n0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cm9n0g04.html b/image/test/reftest/pngsuite-ancillary/cm9n0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cm9n0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cm9n0g04.png b/image/test/reftest/pngsuite-ancillary/cm9n0g04.png new file mode 100644 index 0000000000..dd70911adc Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cm9n0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs3n2c16.html b/image/test/reftest/pngsuite-ancillary/cs3n2c16.html new file mode 100644 index 0000000000..bc4ab1488f --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs3n2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs3n2c16.png b/image/test/reftest/pngsuite-ancillary/cs3n2c16.png new file mode 100644 index 0000000000..bf5fd20a20 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs3n2c16.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs3n3p08.html b/image/test/reftest/pngsuite-ancillary/cs3n3p08.html new file mode 100644 index 0000000000..21557a4004 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs3n3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs3n3p08.png b/image/test/reftest/pngsuite-ancillary/cs3n3p08.png new file mode 100644 index 0000000000..f4a66237bf Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs3n3p08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs5n2c08.html b/image/test/reftest/pngsuite-ancillary/cs5n2c08.html new file mode 100644 index 0000000000..d1642a1bf5 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs5n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs5n2c08.png b/image/test/reftest/pngsuite-ancillary/cs5n2c08.png new file mode 100644 index 0000000000..40f947c33e Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs5n2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs5n3p08.html b/image/test/reftest/pngsuite-ancillary/cs5n3p08.html new file mode 100644 index 0000000000..d1642a1bf5 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs5n3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs5n3p08.png b/image/test/reftest/pngsuite-ancillary/cs5n3p08.png new file mode 100644 index 0000000000..dfd6e6e6ec Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs5n3p08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs8n2c08.html b/image/test/reftest/pngsuite-ancillary/cs8n2c08.html new file mode 100644 index 0000000000..549341e76c --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs8n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs8n2c08.png b/image/test/reftest/pngsuite-ancillary/cs8n2c08.png new file mode 100644 index 0000000000..8e01d3294f Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs8n2c08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/cs8n3p08.html b/image/test/reftest/pngsuite-ancillary/cs8n3p08.html new file mode 100644 index 0000000000..549341e76c --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/cs8n3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/cs8n3p08.png b/image/test/reftest/pngsuite-ancillary/cs8n3p08.png new file mode 100644 index 0000000000..a44066eb6e Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/cs8n3p08.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ct0n0g04.html b/image/test/reftest/pngsuite-ancillary/ct0n0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ct0n0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ct0n0g04.png b/image/test/reftest/pngsuite-ancillary/ct0n0g04.png new file mode 100644 index 0000000000..40d1e062f8 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ct0n0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ct1n0g04.html b/image/test/reftest/pngsuite-ancillary/ct1n0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ct1n0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ct1n0g04.png b/image/test/reftest/pngsuite-ancillary/ct1n0g04.png new file mode 100644 index 0000000000..3ba110aa76 Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ct1n0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/ctzn0g04.html b/image/test/reftest/pngsuite-ancillary/ctzn0g04.html new file mode 100644 index 0000000000..25d3abca33 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/ctzn0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-ancillary/ctzn0g04.png b/image/test/reftest/pngsuite-ancillary/ctzn0g04.png new file mode 100644 index 0000000000..b4401c9cfc Binary files /dev/null and b/image/test/reftest/pngsuite-ancillary/ctzn0g04.png differ diff --git a/image/test/reftest/pngsuite-ancillary/qcms-asm-check.js b/image/test/reftest/pngsuite-ancillary/qcms-asm-check.js new file mode 100644 index 0000000000..32e4434aa7 --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/qcms-asm-check.js @@ -0,0 +1,28 @@ +// This is a workaround for bug 465088, that the qcms assembly doesn't +// quite match the non-assembly output. + +function check_qcms_has_assembly() +{ + // We have assembly code on x86 and x86_64 architectures. + // Unfortunately, detecting that is a little complicated. + + if (navigator.platform == "MacIntel") { + return true; + } + + if (navigator.platform.indexOf("Win") == 0 || navigator.platform == "OS/2") { + // Assume all Windows and OS/2 is x86 or x86_64. We don't + // expose any way for Web content to check. + return true; + } + + // On most Unix-like platforms, navigator.platform is basically + // |uname -sm|. + if (navigator.platform.match(/(i[3456]86|x86_64|amd64|i86)/)) { + return true; + } + + return false; +} + +var qcms_has_assembly = check_qcms_has_assembly(); diff --git a/image/test/reftest/pngsuite-ancillary/reftest.list b/image/test/reftest/pngsuite-ancillary/reftest.list new file mode 100644 index 0000000000..b5608716cc --- /dev/null +++ b/image/test/reftest/pngsuite-ancillary/reftest.list @@ -0,0 +1,62 @@ +# PngSuite - Ancillary chunks + +# cHRM chunks +# +# ccwn2c08 - gamma 1.0000 chunk, chroma chunk w:0.3127,0.3290 r:0.64,0.33 g:0.30,0.60 b:0.15,0.06 +fuzzy-if(appleSilicon,1-1,12-12) fuzzy-if(winWidget,0-8,0-569) fuzzy-if(Android,0-1,0-6) == ccwn2c08.png ccwn2c08.html +# ccwn3p08 - gamma 1.0000 chunk, chroma chunk w:0.3127,0.3290 r:0.64,0.33 g:0.30,0.60 b:0.15,0.06 +fuzzy-if(appleSilicon,1-1,4-4) fuzzy-if(winWidget,0-8,0-577) fuzzy-if(Android,0-1,0-19) == ccwn3p08.png ccwn3p08.html + +# pHYs chunks +# +# PngSuite implies these first 3 should end up as 32x32 bitmaps, but +# per discussion in bug 408622 that's not actually true. +# +# cdfn2c08 - physical pixel dimensions, 8x32 flat pixels +== cdfn2c08.png cdfn2c08.html +# cdhn2c08 - physical pixel dimensions, 32x8 high pixels +== cdhn2c08.png cdhn2c08.html +# cdsn2c08 - physical pixel dimensions, 8x8 square pixels +== cdsn2c08.png cdsn2c08.html +# cdun2c08 - physical pixel dimensions, 1000 pixels per 1 meter +== cdun2c08.png cdun2c08.html + +# hISt chunks (shouldn't affect display on 24bit systems) +# +# ch1n3p04 - histogram 15 colors +== ch1n3p04.png ch1n3p04.html +# ch2n3p08 - histogram 256 colors +== ch2n3p08.png ch2n3p08.html + +# tIME chunks (doesn't affect display) +# +# cm0n0g04 - modification time, 01-jan-2000 12:34:56 +== cm0n0g04.png cm0n0g04.html +# cm7n0g04 - modification time, 01-jan-1970 00:00:00 +== cm7n0g04.png cm7n0g04.html +# cm9n0g04 - modification time, 31-dec-1999 23:59:59 +== cm9n0g04.png cm9n0g04.html + +# sBIT chunks +# +# cs3n2c16 - color, 13 significant bits +== cs3n2c16.png cs3n2c16.html +# cs3n3p08 - paletted, 3 significant bits +== cs3n3p08.png cs3n3p08.html +# cs5n2c08 - color, 5 significant bits +== cs5n2c08.png cs5n2c08.html +# cs5n3p08 - paletted, 5 significant bits +== cs5n3p08.png cs5n3p08.html +# cs8n2c08 - color, 8 significant bits (reference) +== cs8n2c08.png cs8n2c08.html +# cs8n3p08 - paletted, 8 significant bits (reference) +== cs8n3p08.png cs8n3p08.html + +# tEXt chunks (doesn't affect display) +# +# ct0n0g04 - no textual data +== ct0n0g04.png ct0n0g04.html +# ct1n0g04 - with textual data +== ct1n0g04.png ct1n0g04.html +# ctzn0g04 - with compressed textual data +== ctzn0g04.png ctzn0g04.html diff --git a/image/test/reftest/pngsuite-background/bg__4a08.html b/image/test/reftest/pngsuite-background/bg__4a08.html new file mode 100644 index 0000000000..743ad1200a --- /dev/null +++ b/image/test/reftest/pngsuite-background/bg__4a08.html @@ -0,0 +1,1092 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-background/bg__4a16.html b/image/test/reftest/pngsuite-background/bg__4a16.html new file mode 100644 index 0000000000..b15b280f1c --- /dev/null +++ b/image/test/reftest/pngsuite-background/bg__4a16.html @@ -0,0 +1,1092 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-background/bg__6a08.html b/image/test/reftest/pngsuite-background/bg__6a08.html new file mode 100644 index 0000000000..1ab2721f31 --- /dev/null +++ b/image/test/reftest/pngsuite-background/bg__6a08.html @@ -0,0 +1,1092 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-background/bg__6a16.html b/image/test/reftest/pngsuite-background/bg__6a16.html new file mode 100644 index 0000000000..8ead05a34f --- /dev/null +++ b/image/test/reftest/pngsuite-background/bg__6a16.html @@ -0,0 +1,1092 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-background/bgai4a08.png b/image/test/reftest/pngsuite-background/bgai4a08.png new file mode 100644 index 0000000000..398132be5f Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgai4a08.png differ diff --git a/image/test/reftest/pngsuite-background/bgai4a16.png b/image/test/reftest/pngsuite-background/bgai4a16.png new file mode 100644 index 0000000000..51192e7311 Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgai4a16.png differ diff --git a/image/test/reftest/pngsuite-background/bgan6a08.png b/image/test/reftest/pngsuite-background/bgan6a08.png new file mode 100644 index 0000000000..e608738763 Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgan6a08.png differ diff --git a/image/test/reftest/pngsuite-background/bgan6a16.png b/image/test/reftest/pngsuite-background/bgan6a16.png new file mode 100644 index 0000000000..984a99525f Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgan6a16.png differ diff --git a/image/test/reftest/pngsuite-background/bgbn4a08.png b/image/test/reftest/pngsuite-background/bgbn4a08.png new file mode 100644 index 0000000000..7cbefc3bff Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgbn4a08.png differ diff --git a/image/test/reftest/pngsuite-background/bggn4a16.png b/image/test/reftest/pngsuite-background/bggn4a16.png new file mode 100644 index 0000000000..13fd85ba19 Binary files /dev/null and b/image/test/reftest/pngsuite-background/bggn4a16.png differ diff --git a/image/test/reftest/pngsuite-background/bgwn6a08.png b/image/test/reftest/pngsuite-background/bgwn6a08.png new file mode 100644 index 0000000000..a67ff205bb Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgwn6a08.png differ diff --git a/image/test/reftest/pngsuite-background/bgyn6a16.png b/image/test/reftest/pngsuite-background/bgyn6a16.png new file mode 100644 index 0000000000..ae3e9be58a Binary files /dev/null and b/image/test/reftest/pngsuite-background/bgyn6a16.png differ diff --git a/image/test/reftest/pngsuite-background/reftest.list b/image/test/reftest/pngsuite-background/reftest.list new file mode 100644 index 0000000000..1b6c514de1 --- /dev/null +++ b/image/test/reftest/pngsuite-background/reftest.list @@ -0,0 +1,22 @@ +# PngSuite - Background colors +# +# Note 1: The first 4 images have no bKGD chunk, the last 4 do. The background +# color indicated by bKGD isn't used, so the two sets of images are rendered +# identically and thus share common reference HTML files. + +# bgai4a08 - 8 bit grayscale, alpha, no background chunk, interlaced +fuzzy(0-2,0-1024) == wrapper.html?bgai4a08.png bg__4a08.html +# bgai4a16 - 16 bit grayscale, alpha, no background chunk, interlaced +fuzzy(0-2,0-1024) == wrapper.html?bgai4a16.png bg__4a16.html +# bgan6a08 - 3x8 bits rgb color, alpha, no background chunk +fuzzy(0-2,0-1024) == wrapper.html?bgan6a08.png bg__6a08.html +# bgan6a16 - 3x16 bits rgb color, alpha, no background chunk +fuzzy(0-2,0-1024) == wrapper.html?bgan6a16.png bg__6a16.html +# bgbn4a08 - 8 bit grayscale, alpha, black background chunk +fuzzy(0-2,0-1024) == wrapper.html?bgbn4a08.png bg__4a08.html +# bggn4a16 - 16 bit grayscale, alpha, gray background chunk +fuzzy(0-2,0-1024) == wrapper.html?bggn4a16.png bg__4a16.html +# bgwn6a08 - 3x8 bits rgb color, alpha, white background chunk +fuzzy(0-2,0-1024) == wrapper.html?bgwn6a08.png bg__6a08.html +# bgyn6a16 - 3x16 bits rgb color, alpha, yellow background chunk +fuzzy(0-2,0-1024) == wrapper.html?bgyn6a16.png bg__6a16.html diff --git a/image/test/reftest/pngsuite-background/wrapper.html b/image/test/reftest/pngsuite-background/wrapper.html new file mode 100644 index 0000000000..45b5167754 --- /dev/null +++ b/image/test/reftest/pngsuite-background/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g01.html b/image/test/reftest/pngsuite-basic-i/basi0g01.html new file mode 100644 index 0000000000..7389a1b66e --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi0g01.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g01.png b/image/test/reftest/pngsuite-basic-i/basi0g01.png new file mode 100644 index 0000000000..556fa72704 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi0g01.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi0g02.html b/image/test/reftest/pngsuite-basic-i/basi0g02.html new file mode 100644 index 0000000000..538afad142 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi0g02.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g02.png b/image/test/reftest/pngsuite-basic-i/basi0g02.png new file mode 100644 index 0000000000..ce09821ef1 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi0g02.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi0g04.html b/image/test/reftest/pngsuite-basic-i/basi0g04.html new file mode 100644 index 0000000000..d782230d4b --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g04.png b/image/test/reftest/pngsuite-basic-i/basi0g04.png new file mode 100644 index 0000000000..3853273f93 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi0g04.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi0g08.html b/image/test/reftest/pngsuite-basic-i/basi0g08.html new file mode 100644 index 0000000000..5aaf11cabb --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g08.png b/image/test/reftest/pngsuite-basic-i/basi0g08.png new file mode 100644 index 0000000000..faed8bec44 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi0g08.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi0g16.html b/image/test/reftest/pngsuite-basic-i/basi0g16.html new file mode 100644 index 0000000000..fc18c727be --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi0g16.png b/image/test/reftest/pngsuite-basic-i/basi0g16.png new file mode 100644 index 0000000000..a9f28165ef Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi0g16.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi2c08.html b/image/test/reftest/pngsuite-basic-i/basi2c08.html new file mode 100644 index 0000000000..e30216bdf0 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi2c08.png b/image/test/reftest/pngsuite-basic-i/basi2c08.png new file mode 100644 index 0000000000..2aab44d42b Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi2c08.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi2c16.html b/image/test/reftest/pngsuite-basic-i/basi2c16.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi2c16.png b/image/test/reftest/pngsuite-basic-i/basi2c16.png new file mode 100644 index 0000000000..cd7e50f914 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi2c16.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi3p01.html b/image/test/reftest/pngsuite-basic-i/basi3p01.html new file mode 100644 index 0000000000..2cb512200a --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi3p01.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi3p01.png b/image/test/reftest/pngsuite-basic-i/basi3p01.png new file mode 100644 index 0000000000..00a7cea6c2 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi3p01.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi3p02.html b/image/test/reftest/pngsuite-basic-i/basi3p02.html new file mode 100644 index 0000000000..4555fbb9b9 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi3p02.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi3p02.png b/image/test/reftest/pngsuite-basic-i/basi3p02.png new file mode 100644 index 0000000000..bb16b44b30 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi3p02.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi3p04.html b/image/test/reftest/pngsuite-basic-i/basi3p04.html new file mode 100644 index 0000000000..dc2a121de2 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi3p04.png b/image/test/reftest/pngsuite-basic-i/basi3p04.png new file mode 100644 index 0000000000..b4e888e247 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi3p04.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi3p08.html b/image/test/reftest/pngsuite-basic-i/basi3p08.html new file mode 100644 index 0000000000..78b72c61c6 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/basi3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-i/basi3p08.png b/image/test/reftest/pngsuite-basic-i/basi3p08.png new file mode 100644 index 0000000000..50a6d1cac7 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi3p08.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi4a08.png b/image/test/reftest/pngsuite-basic-i/basi4a08.png new file mode 100644 index 0000000000..398132be5f Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi4a08.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi4a16.png b/image/test/reftest/pngsuite-basic-i/basi4a16.png new file mode 100644 index 0000000000..51192e7311 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi4a16.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi6a08.png b/image/test/reftest/pngsuite-basic-i/basi6a08.png new file mode 100644 index 0000000000..aecb32e0d9 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi6a08.png differ diff --git a/image/test/reftest/pngsuite-basic-i/basi6a16.png b/image/test/reftest/pngsuite-basic-i/basi6a16.png new file mode 100644 index 0000000000..4181533ad8 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-i/basi6a16.png differ diff --git a/image/test/reftest/pngsuite-basic-i/reftest.list b/image/test/reftest/pngsuite-basic-i/reftest.list new file mode 100644 index 0000000000..bc61af89d8 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-i/reftest.list @@ -0,0 +1,33 @@ +# PngSuite - Basic formats (interlaced) + + +# basi0g01 - black & white +== basi0g01.png basi0g01.html +# basi0g02 - 2 bit (4 level) grayscale +== basi0g02.png basi0g02.html +# basi0g04 - 4 bit (16 level) grayscale +== basi0g04.png basi0g04.html +# basi0g08 - 8 bit (256 level) grayscale +== basi0g08.png basi0g08.html +# basi0g16 - 16 bit (64k level) grayscale +== basi0g16.png basi0g16.html +# basi2c08 - 3x8 bits rgb color +== basi2c08.png basi2c08.html +# basi2c16 - 3x16 bits rgb color +== basi2c16.png basi2c16.html +# basi3p01 - 1 bit (2 color) paletted +== basi3p01.png basi3p01.html +# basi3p02 - 2 bit (4 color) paletted +== basi3p02.png basi3p02.html +# basi3p04 - 4 bit (16 color) paletted +== basi3p04.png basi3p04.html +# basi3p08 - 8 bit (256 color) paletted +== basi3p08.png basi3p08.html +# basi4a08 - 8 bit grayscale + 8 bit alpha-channel +#== basi4a08.png basi4a08.html +# basi4a16 - 16 bit grayscale + 16 bit alpha-channel +#== basi4a16.png basi4a16.html +# basi6a08 - 3x8 bits rgb color + 8 bit alpha-channel +#== basi6a08.png basi6a08.html +# basi6a16 - 3x16 bits rgb color + 16 bit alpha-channel +#== basi6a16.png basi6a16.html diff --git a/image/test/reftest/pngsuite-basic-n/basn0g01.html b/image/test/reftest/pngsuite-basic-n/basn0g01.html new file mode 100644 index 0000000000..7389a1b66e --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn0g01.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn0g01.png b/image/test/reftest/pngsuite-basic-n/basn0g01.png new file mode 100644 index 0000000000..1d722423aa Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn0g01.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn0g02.html b/image/test/reftest/pngsuite-basic-n/basn0g02.html new file mode 100644 index 0000000000..538afad142 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn0g02.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn0g02.png b/image/test/reftest/pngsuite-basic-n/basn0g02.png new file mode 100644 index 0000000000..508332418f Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn0g02.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn0g04.html b/image/test/reftest/pngsuite-basic-n/basn0g04.html new file mode 100644 index 0000000000..d782230d4b --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn0g04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn0g04.png b/image/test/reftest/pngsuite-basic-n/basn0g04.png new file mode 100644 index 0000000000..0bf3687863 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn0g04.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn0g08.html b/image/test/reftest/pngsuite-basic-n/basn0g08.html new file mode 100644 index 0000000000..5aaf11cabb --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn0g08.png b/image/test/reftest/pngsuite-basic-n/basn0g08.png new file mode 100644 index 0000000000..23c82379a2 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn0g08.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn0g16.html b/image/test/reftest/pngsuite-basic-n/basn0g16.html new file mode 100644 index 0000000000..fc18c727be --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn0g16.png b/image/test/reftest/pngsuite-basic-n/basn0g16.png new file mode 100644 index 0000000000..e7c82f78eb Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn0g16.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn2c08.html b/image/test/reftest/pngsuite-basic-n/basn2c08.html new file mode 100644 index 0000000000..e30216bdf0 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn2c08.png b/image/test/reftest/pngsuite-basic-n/basn2c08.png new file mode 100644 index 0000000000..db5ad15865 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn2c08.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn2c16.html b/image/test/reftest/pngsuite-basic-n/basn2c16.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn2c16.png b/image/test/reftest/pngsuite-basic-n/basn2c16.png new file mode 100644 index 0000000000..50c1cb91a0 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn2c16.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn3p01.html b/image/test/reftest/pngsuite-basic-n/basn3p01.html new file mode 100644 index 0000000000..2cb512200a --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn3p01.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn3p01.png b/image/test/reftest/pngsuite-basic-n/basn3p01.png new file mode 100644 index 0000000000..b145c2b8ef Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn3p01.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn3p02.html b/image/test/reftest/pngsuite-basic-n/basn3p02.html new file mode 100644 index 0000000000..4555fbb9b9 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn3p02.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn3p02.png b/image/test/reftest/pngsuite-basic-n/basn3p02.png new file mode 100644 index 0000000000..8985b3d818 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn3p02.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn3p04.html b/image/test/reftest/pngsuite-basic-n/basn3p04.html new file mode 100644 index 0000000000..dc2a121de2 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn3p04.png b/image/test/reftest/pngsuite-basic-n/basn3p04.png new file mode 100644 index 0000000000..0fbf9e827b Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn3p04.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn3p08.html b/image/test/reftest/pngsuite-basic-n/basn3p08.html new file mode 100644 index 0000000000..78b72c61c6 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/basn3p08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-basic-n/basn3p08.png b/image/test/reftest/pngsuite-basic-n/basn3p08.png new file mode 100644 index 0000000000..0ddad07e5f Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn3p08.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn4a08.png b/image/test/reftest/pngsuite-basic-n/basn4a08.png new file mode 100644 index 0000000000..3e13052201 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn4a08.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn4a16.png b/image/test/reftest/pngsuite-basic-n/basn4a16.png new file mode 100644 index 0000000000..8243644d07 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn4a16.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn6a08.png b/image/test/reftest/pngsuite-basic-n/basn6a08.png new file mode 100644 index 0000000000..e608738763 Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn6a08.png differ diff --git a/image/test/reftest/pngsuite-basic-n/basn6a16.png b/image/test/reftest/pngsuite-basic-n/basn6a16.png new file mode 100644 index 0000000000..984a99525f Binary files /dev/null and b/image/test/reftest/pngsuite-basic-n/basn6a16.png differ diff --git a/image/test/reftest/pngsuite-basic-n/reftest.list b/image/test/reftest/pngsuite-basic-n/reftest.list new file mode 100644 index 0000000000..c59a5a7e47 --- /dev/null +++ b/image/test/reftest/pngsuite-basic-n/reftest.list @@ -0,0 +1,33 @@ +# PngSuite - Basic formats (non-interlaced) + + +# basn0g01 - black & white +== basn0g01.png basn0g01.html +# basn0g02 - 2 bit (4 level) grayscale +== basn0g02.png basn0g02.html +# basn0g04 - 4 bit (16 level) grayscale +== basn0g04.png basn0g04.html +# basn0g08 - 8 bit (256 level) grayscale +== basn0g08.png basn0g08.html +# basn0g16 - 16 bit (64k level) grayscale +== basn0g16.png basn0g16.html +# basn2c08 - 3x8 bits rgb color +== basn2c08.png basn2c08.html +# basn2c16 - 3x16 bits rgb color +== basn2c16.png basn2c16.html +# basn3p01 - 1 bit (2 color) paletted +== basn3p01.png basn3p01.html +# basn3p02 - 2 bit (4 color) paletted +== basn3p02.png basn3p02.html +# basn3p04 - 4 bit (16 color) paletted +== basn3p04.png basn3p04.html +# basn3p08 - 8 bit (256 color) paletted +== basn3p08.png basn3p08.html +# basn4a08 - 8 bit grayscale + 8 bit alpha-channel +#== basn4a08.png basn4a08.html +# basn4a16 - 16 bit grayscale + 16 bit alpha-channel +#== basn4a16.png basn4a16.html +# basn6a08 - 3x8 bits rgb color + 8 bit alpha-channel +#== basn6a08.png basn6a08.html +# basn6a16 - 3x16 bits rgb color + 16 bit alpha-channel +#== basn6a16.png basn6a16.html diff --git a/image/test/reftest/pngsuite-chunkorder/color.html b/image/test/reftest/pngsuite-chunkorder/color.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-chunkorder/color.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-chunkorder/grayscale.html b/image/test/reftest/pngsuite-chunkorder/grayscale.html new file mode 100644 index 0000000000..fc18c727be --- /dev/null +++ b/image/test/reftest/pngsuite-chunkorder/grayscale.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-chunkorder/oi1n0g16.png b/image/test/reftest/pngsuite-chunkorder/oi1n0g16.png new file mode 100644 index 0000000000..e7c82f78eb Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi1n0g16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi1n2c16.png b/image/test/reftest/pngsuite-chunkorder/oi1n2c16.png new file mode 100644 index 0000000000..50c1cb91a0 Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi1n2c16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi2n0g16.png b/image/test/reftest/pngsuite-chunkorder/oi2n0g16.png new file mode 100644 index 0000000000..14d64c583d Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi2n0g16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi2n2c16.png b/image/test/reftest/pngsuite-chunkorder/oi2n2c16.png new file mode 100644 index 0000000000..4c2e3e3352 Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi2n2c16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi4n0g16.png b/image/test/reftest/pngsuite-chunkorder/oi4n0g16.png new file mode 100644 index 0000000000..69e73ede31 Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi4n0g16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi4n2c16.png b/image/test/reftest/pngsuite-chunkorder/oi4n2c16.png new file mode 100644 index 0000000000..93691e373a Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi4n2c16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi9n0g16.png b/image/test/reftest/pngsuite-chunkorder/oi9n0g16.png new file mode 100644 index 0000000000..9248413576 Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi9n0g16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/oi9n2c16.png b/image/test/reftest/pngsuite-chunkorder/oi9n2c16.png new file mode 100644 index 0000000000..f0512e49f2 Binary files /dev/null and b/image/test/reftest/pngsuite-chunkorder/oi9n2c16.png differ diff --git a/image/test/reftest/pngsuite-chunkorder/reftest.list b/image/test/reftest/pngsuite-chunkorder/reftest.list new file mode 100644 index 0000000000..2e161d0d39 --- /dev/null +++ b/image/test/reftest/pngsuite-chunkorder/reftest.list @@ -0,0 +1,21 @@ +# PngSuite - Chunk ordering +# +# The resulting images of a type (color or grayscale) should all look the +# same, so they share common HTML reference files. + +# oi1n0g16 - grayscale mother image with 1 idat-chunk +== oi1n0g16.png grayscale.html +# oi1n2c16 - color mother image with 1 idat-chunk +== oi1n2c16.png color.html +# oi2n0g16 - grayscale image with 2 idat-chunks +== oi2n0g16.png grayscale.html +# oi2n2c16 - color image with 2 idat-chunks +== oi2n2c16.png color.html +# oi4n0g16 - grayscale image with 4 unequal sized idat-chunks +== oi4n0g16.png grayscale.html +# oi4n2c16 - color image with 4 unequal sized idat-chunks +== oi4n2c16.png color.html +# oi9n0g16 - grayscale image with all idat-chunks length one +== oi9n0g16.png grayscale.html +# oi9n2c16 - color image with all idat-chunks length one +== oi9n2c16.png color.html diff --git a/image/test/reftest/pngsuite-corrupted/reftest.list b/image/test/reftest/pngsuite-corrupted/reftest.list new file mode 100644 index 0000000000..86c5880f2c --- /dev/null +++ b/image/test/reftest/pngsuite-corrupted/reftest.list @@ -0,0 +1,10 @@ +# PngSuite - Corrupted files +# +# Note: these are corrupt files, and so no image should be rendered. + +# x00n0g01 - empty 0x0 grayscale file +== wrapper.html?x00n0g01.png about:blank +# xcrn0g04 - added cr bytes +== wrapper.html?xcrn0g04.png about:blank +# xlfn0g04 - added lf bytes +== wrapper.html?xlfn0g04.png about:blank diff --git a/image/test/reftest/pngsuite-corrupted/wrapper.html b/image/test/reftest/pngsuite-corrupted/wrapper.html new file mode 100644 index 0000000000..45b5167754 --- /dev/null +++ b/image/test/reftest/pngsuite-corrupted/wrapper.html @@ -0,0 +1,28 @@ + + + +Image reftest wrapper + + + + + + + + + + diff --git a/image/test/reftest/pngsuite-corrupted/x00n0g01.png b/image/test/reftest/pngsuite-corrupted/x00n0g01.png new file mode 100644 index 0000000000..db3a5fda7e Binary files /dev/null and b/image/test/reftest/pngsuite-corrupted/x00n0g01.png differ diff --git a/image/test/reftest/pngsuite-corrupted/xcrn0g04.png b/image/test/reftest/pngsuite-corrupted/xcrn0g04.png new file mode 100644 index 0000000000..5bce9f3ada Binary files /dev/null and b/image/test/reftest/pngsuite-corrupted/xcrn0g04.png differ diff --git a/image/test/reftest/pngsuite-corrupted/xlfn0g04.png b/image/test/reftest/pngsuite-corrupted/xlfn0g04.png new file mode 100644 index 0000000000..1fd104ba61 --- /dev/null +++ b/image/test/reftest/pngsuite-corrupted/xlfn0g04.png @@ -0,0 +1,13 @@ +‰PNG + + + + + +IHDR “áÈ)ÈIDATxœ]ÑÁ +Â0 P*@ð¡#° + +#TâÈ10lPF`Ø F=•ŸÄIQâ*çÅuí”`%qk +Hžñšˆ©ñ´€m÷Íüµàߟ Ñ=,¸fìOK + +ç ÐtŽÀ(Èïä’צíF ;èPº€¯¾{xpç]9‡/p*$(ì*éyìÕƒ ×þÚéçè@÷C¼  cÔqž‹NÛU#„)11·.räðfä0°ägh(¥týÙÂEøÿ‰kIEND®B`‚ \ No newline at end of file diff --git a/image/test/reftest/pngsuite-filtering/f00n0g08.html b/image/test/reftest/pngsuite-filtering/f00n0g08.html new file mode 100644 index 0000000000..3df624891b --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f00n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f00n0g08.png b/image/test/reftest/pngsuite-filtering/f00n0g08.png new file mode 100644 index 0000000000..45a0075967 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f00n0g08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f00n2c08.html b/image/test/reftest/pngsuite-filtering/f00n2c08.html new file mode 100644 index 0000000000..2e5f1e1868 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f00n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f00n2c08.png b/image/test/reftest/pngsuite-filtering/f00n2c08.png new file mode 100644 index 0000000000..d6a1ffff62 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f00n2c08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f01n0g08.html b/image/test/reftest/pngsuite-filtering/f01n0g08.html new file mode 100644 index 0000000000..2e056ecb9f --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f01n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f01n0g08.png b/image/test/reftest/pngsuite-filtering/f01n0g08.png new file mode 100644 index 0000000000..4a1107b463 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f01n0g08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f01n2c08.html b/image/test/reftest/pngsuite-filtering/f01n2c08.html new file mode 100644 index 0000000000..25c4fe0446 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f01n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f01n2c08.png b/image/test/reftest/pngsuite-filtering/f01n2c08.png new file mode 100644 index 0000000000..26fee958ce Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f01n2c08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f02n0g08.html b/image/test/reftest/pngsuite-filtering/f02n0g08.html new file mode 100644 index 0000000000..c9a6263f47 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f02n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f02n0g08.png b/image/test/reftest/pngsuite-filtering/f02n0g08.png new file mode 100644 index 0000000000..bfe410c5e7 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f02n0g08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f02n2c08.html b/image/test/reftest/pngsuite-filtering/f02n2c08.html new file mode 100644 index 0000000000..051691ab99 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f02n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f02n2c08.png b/image/test/reftest/pngsuite-filtering/f02n2c08.png new file mode 100644 index 0000000000..e590f12348 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f02n2c08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f03n0g08.html b/image/test/reftest/pngsuite-filtering/f03n0g08.html new file mode 100644 index 0000000000..f40bbe51b7 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f03n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f03n0g08.png b/image/test/reftest/pngsuite-filtering/f03n0g08.png new file mode 100644 index 0000000000..ed01e2923c Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f03n0g08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f03n2c08.html b/image/test/reftest/pngsuite-filtering/f03n2c08.html new file mode 100644 index 0000000000..3d3c85e6cf --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f03n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f03n2c08.png b/image/test/reftest/pngsuite-filtering/f03n2c08.png new file mode 100644 index 0000000000..758115059d Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f03n2c08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f04n0g08.html b/image/test/reftest/pngsuite-filtering/f04n0g08.html new file mode 100644 index 0000000000..3c7ce550ba --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f04n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f04n0g08.png b/image/test/reftest/pngsuite-filtering/f04n0g08.png new file mode 100644 index 0000000000..663fdae3e7 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f04n0g08.png differ diff --git a/image/test/reftest/pngsuite-filtering/f04n2c08.html b/image/test/reftest/pngsuite-filtering/f04n2c08.html new file mode 100644 index 0000000000..77c90face2 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/f04n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-filtering/f04n2c08.png b/image/test/reftest/pngsuite-filtering/f04n2c08.png new file mode 100644 index 0000000000..3c8b5116e7 Binary files /dev/null and b/image/test/reftest/pngsuite-filtering/f04n2c08.png differ diff --git a/image/test/reftest/pngsuite-filtering/reftest.list b/image/test/reftest/pngsuite-filtering/reftest.list new file mode 100644 index 0000000000..3a39efbbe3 --- /dev/null +++ b/image/test/reftest/pngsuite-filtering/reftest.list @@ -0,0 +1,22 @@ +# PngSuite - Image filtering + +# f00n0g08 - grayscale, no interlacing, filter-type 0 +skip-if(ThreadSanitizer) == f00n0g08.png f00n0g08.html +# f00n2c08 - color, no interlacing, filter-type 0 +== f00n2c08.png f00n2c08.html +# f01n0g08 - grayscale, no interlacing, filter-type 1 +== f01n0g08.png f01n0g08.html +# f01n2c08 - color, no interlacing, filter-type 1 +== f01n2c08.png f01n2c08.html +# f02n0g08 - grayscale, no interlacing, filter-type 2 +== f02n0g08.png f02n0g08.html +# f02n2c08 - color, no interlacing, filter-type 2 +== f02n2c08.png f02n2c08.html +# f03n0g08 - grayscale, no interlacing, filter-type 3 +== f03n0g08.png f03n0g08.html +# f03n2c08 - color, no interlacing, filter-type 3 +== f03n2c08.png f03n2c08.html +# f04n0g08 - grayscale, no interlacing, filter-type 4 +== f04n0g08.png f04n0g08.html +# f04n2c08 - color, no interlacing, filter-type 4 +== f04n2c08.png f04n2c08.html diff --git a/image/test/reftest/pngsuite-gamma/g03n0g16.html b/image/test/reftest/pngsuite-gamma/g03n0g16.html new file mode 100644 index 0000000000..dc15a536b5 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g03n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g03n0g16.png b/image/test/reftest/pngsuite-gamma/g03n0g16.png new file mode 100644 index 0000000000..41083ca80f Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g03n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g03n2c08.html b/image/test/reftest/pngsuite-gamma/g03n2c08.html new file mode 100644 index 0000000000..c2d02beed9 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g03n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g03n2c08.png b/image/test/reftest/pngsuite-gamma/g03n2c08.png new file mode 100644 index 0000000000..a9354dbee6 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g03n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g03n3p04.html b/image/test/reftest/pngsuite-gamma/g03n3p04.html new file mode 100644 index 0000000000..efcf39f291 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g03n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g03n3p04.png b/image/test/reftest/pngsuite-gamma/g03n3p04.png new file mode 100644 index 0000000000..60396c95af Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g03n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/g04n0g16.html b/image/test/reftest/pngsuite-gamma/g04n0g16.html new file mode 100644 index 0000000000..5bec9867fa --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g04n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g04n0g16.png b/image/test/reftest/pngsuite-gamma/g04n0g16.png new file mode 100644 index 0000000000..32395b76c9 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g04n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g04n2c08.html b/image/test/reftest/pngsuite-gamma/g04n2c08.html new file mode 100644 index 0000000000..b3b0556c6e --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g04n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g04n2c08.png b/image/test/reftest/pngsuite-gamma/g04n2c08.png new file mode 100644 index 0000000000..a652b0ce87 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g04n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g04n3p04.html b/image/test/reftest/pngsuite-gamma/g04n3p04.html new file mode 100644 index 0000000000..337dcb49d7 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g04n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g04n3p04.png b/image/test/reftest/pngsuite-gamma/g04n3p04.png new file mode 100644 index 0000000000..5661cc3131 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g04n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/g05n0g16.html b/image/test/reftest/pngsuite-gamma/g05n0g16.html new file mode 100644 index 0000000000..ab100e638b --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g05n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g05n0g16.png b/image/test/reftest/pngsuite-gamma/g05n0g16.png new file mode 100644 index 0000000000..70b37f01e2 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g05n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g05n2c08.html b/image/test/reftest/pngsuite-gamma/g05n2c08.html new file mode 100644 index 0000000000..475ecd21df --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g05n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g05n2c08.png b/image/test/reftest/pngsuite-gamma/g05n2c08.png new file mode 100644 index 0000000000..932c136536 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g05n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g05n3p04.html b/image/test/reftest/pngsuite-gamma/g05n3p04.html new file mode 100644 index 0000000000..d71689c29a --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g05n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g05n3p04.png b/image/test/reftest/pngsuite-gamma/g05n3p04.png new file mode 100644 index 0000000000..9619930585 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g05n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/g07n0g16.html b/image/test/reftest/pngsuite-gamma/g07n0g16.html new file mode 100644 index 0000000000..b9f1a1c3ea --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g07n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g07n0g16.png b/image/test/reftest/pngsuite-gamma/g07n0g16.png new file mode 100644 index 0000000000..d6a47c2d57 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g07n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g07n2c08.html b/image/test/reftest/pngsuite-gamma/g07n2c08.html new file mode 100644 index 0000000000..0a5b63bf01 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g07n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g07n2c08.png b/image/test/reftest/pngsuite-gamma/g07n2c08.png new file mode 100644 index 0000000000..597346460f Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g07n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g07n3p04.html b/image/test/reftest/pngsuite-gamma/g07n3p04.html new file mode 100644 index 0000000000..7303ed0d70 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g07n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g07n3p04.png b/image/test/reftest/pngsuite-gamma/g07n3p04.png new file mode 100644 index 0000000000..c73fb61365 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g07n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/g10n0g16.html b/image/test/reftest/pngsuite-gamma/g10n0g16.html new file mode 100644 index 0000000000..29301dd710 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g10n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g10n0g16.png b/image/test/reftest/pngsuite-gamma/g10n0g16.png new file mode 100644 index 0000000000..85f2c958e9 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g10n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g10n2c08.html b/image/test/reftest/pngsuite-gamma/g10n2c08.html new file mode 100644 index 0000000000..24e8637b74 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g10n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g10n2c08.png b/image/test/reftest/pngsuite-gamma/g10n2c08.png new file mode 100644 index 0000000000..b3039970c1 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g10n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g10n3p04.html b/image/test/reftest/pngsuite-gamma/g10n3p04.html new file mode 100644 index 0000000000..7c25d439d8 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g10n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g10n3p04.png b/image/test/reftest/pngsuite-gamma/g10n3p04.png new file mode 100644 index 0000000000..1b6a6be2ca Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g10n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/g25n0g16.html b/image/test/reftest/pngsuite-gamma/g25n0g16.html new file mode 100644 index 0000000000..7f3d84edf4 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g25n0g16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g25n0g16.png b/image/test/reftest/pngsuite-gamma/g25n0g16.png new file mode 100644 index 0000000000..a9f6787c7a Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g25n0g16.png differ diff --git a/image/test/reftest/pngsuite-gamma/g25n2c08.html b/image/test/reftest/pngsuite-gamma/g25n2c08.html new file mode 100644 index 0000000000..2476d2cc9d --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g25n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g25n2c08.png b/image/test/reftest/pngsuite-gamma/g25n2c08.png new file mode 100644 index 0000000000..03f505a64b Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g25n2c08.png differ diff --git a/image/test/reftest/pngsuite-gamma/g25n3p04.html b/image/test/reftest/pngsuite-gamma/g25n3p04.html new file mode 100644 index 0000000000..3cb0205bed --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/g25n3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-gamma/g25n3p04.png b/image/test/reftest/pngsuite-gamma/g25n3p04.png new file mode 100644 index 0000000000..4f943c6175 Binary files /dev/null and b/image/test/reftest/pngsuite-gamma/g25n3p04.png differ diff --git a/image/test/reftest/pngsuite-gamma/reftest.list b/image/test/reftest/pngsuite-gamma/reftest.list new file mode 100644 index 0000000000..1b5abdda75 --- /dev/null +++ b/image/test/reftest/pngsuite-gamma/reftest.list @@ -0,0 +1,38 @@ +# PngSuite - Gamma values + +# g03n0g16 - grayscale, file-gamma = 0.35 +== g03n0g16.png g03n0g16.html +# g03n2c08 - color, file-gamma = 0.35 +== g03n2c08.png g03n2c08.html +# g03n3p04 - paletted, file-gamma = 0.35 +== g03n3p04.png g03n3p04.html +# g04n0g16 - grayscale, file-gamma = 0.45 +== g04n0g16.png g04n0g16.html +# g04n2c08 - color, file-gamma = 0.45 +== g04n2c08.png g04n2c08.html +# g04n3p04 - paletted, file-gamma = 0.45 +== g04n3p04.png g04n3p04.html +# g05n0g16 - grayscale, file-gamma = 0.55 +== g05n0g16.png g05n0g16.html +# g05n2c08 - color, file-gamma = 0.55 +== g05n2c08.png g05n2c08.html +# g05n3p04 - paletted, file-gamma = 0.55 +== g05n3p04.png g05n3p04.html +# g07n0g16 - grayscale, file-gamma = 0.70 +== g07n0g16.png g07n0g16.html +# g07n2c08 - color, file-gamma = 0.70 +== g07n2c08.png g07n2c08.html +# g07n3p04 - paletted, file-gamma = 0.70 +== g07n3p04.png g07n3p04.html +# g10n0g16 - grayscale, file-gamma = 1.00 +== g10n0g16.png g10n0g16.html +# g10n2c08 - color, file-gamma = 1.00 +== g10n2c08.png g10n2c08.html +# g10n3p04 - paletted, file-gamma = 1.00 +== g10n3p04.png g10n3p04.html +# g25n0g16 - grayscale, file-gamma = 2.50 +== g25n0g16.png g25n0g16.html +# g25n2c08 - color, file-gamma = 2.50 +== g25n2c08.png g25n2c08.html +# g25n3p04 - paletted, file-gamma = 2.50 +== g25n3p04.png g25n3p04.html diff --git a/image/test/reftest/pngsuite-oddsizes/reftest.list b/image/test/reftest/pngsuite-oddsizes/reftest.list new file mode 100644 index 0000000000..fa72e005b6 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/reftest.list @@ -0,0 +1,77 @@ +# PngSuite - Odd sizes +# +# Note: For each size, there are 2 PNGs (one interlaced, one not). Both +# versions look identical, so they share a common HTML reference file. + +# s01i3p01 - 1x1 paletted file, interlaced +== s01i3p01.png s01_3p01.html +# s01n3p01 - 1x1 paletted file, no interlacing +== s01n3p01.png s01_3p01.html +# s02i3p01 - 2x2 paletted file, interlaced +== s02i3p01.png s02_3p01.html +# s02n3p01 - 2x2 paletted file, no interlacing +== s02n3p01.png s02_3p01.html +# s03i3p01 - 3x3 paletted file, interlaced +== s03i3p01.png s03_3p01.html +# s03n3p01 - 3x3 paletted file, no interlacing +== s03n3p01.png s03_3p01.html +# s04i3p01 - 4x4 paletted file, interlaced +== s04i3p01.png s04_3p01.html +# s04n3p01 - 4x4 paletted file, no interlacing +== s04n3p01.png s04_3p01.html +# s05i3p02 - 5x5 paletted file, interlaced +== s05i3p02.png s05_3p02.html +# s05n3p02 - 5x5 paletted file, no interlacing +== s05n3p02.png s05_3p02.html +# s06i3p02 - 6x6 paletted file, interlaced +== s06i3p02.png s06_3p02.html +# s06n3p02 - 6x6 paletted file, no interlacing +== s06n3p02.png s06_3p02.html +# s07i3p02 - 7x7 paletted file, interlaced +== s07i3p02.png s07_3p02.html +# s07n3p02 - 7x7 paletted file, no interlacing +== s07n3p02.png s07_3p02.html +# s08i3p02 - 8x8 paletted file, interlaced +== s08i3p02.png s08_3p02.html +# s08n3p02 - 8x8 paletted file, no interlacing +== s08n3p02.png s08_3p02.html +# s09i3p02 - 9x9 paletted file, interlaced +== s09i3p02.png s09_3p02.html +# s09n3p02 - 9x9 paletted file, no interlacing +== s09n3p02.png s09_3p02.html +# s32i3p04 - 32x32 paletted file, interlaced +== s32i3p04.png s32_3p04.html +# s32n3p04 - 32x32 paletted file, no interlacing +== s32n3p04.png s32_3p04.html +# s33i3p04 - 33x33 paletted file, interlaced +== s33i3p04.png s33_3p04.html +# s33n3p04 - 33x33 paletted file, no interlacing +== s33n3p04.png s33_3p04.html +# s34i3p04 - 34x34 paletted file, interlaced +== s34i3p04.png s34_3p04.html +# s34n3p04 - 34x34 paletted file, no interlacing +== s34n3p04.png s34_3p04.html +# s35i3p04 - 35x35 paletted file, interlaced +== s35i3p04.png s35_3p04.html +# s35n3p04 - 35x35 paletted file, no interlacing +== s35n3p04.png s35_3p04.html +# s36i3p04 - 36x36 paletted file, interlaced +== s36i3p04.png s36_3p04.html +# s36n3p04 - 36x36 paletted file, no interlacing +== s36n3p04.png s36_3p04.html +# s37i3p04 - 37x37 paletted file, interlaced +== s37i3p04.png s37_3p04.html +# s37n3p04 - 37x37 paletted file, no interlacing +== s37n3p04.png s37_3p04.html +# s38i3p04 - 38x38 paletted file, interlaced +== s38i3p04.png s38_3p04.html +# s38n3p04 - 38x38 paletted file, no interlacing +== s38n3p04.png s38_3p04.html +# s39i3p04 - 39x39 paletted file, interlaced +== s39i3p04.png s39_3p04.html +# s39n3p04 - 39x39 paletted file, no interlacing +== s39n3p04.png s39_3p04.html +# s40i3p04 - 40x40 paletted file, interlaced +== s40i3p04.png s40_3p04.html +# s40n3p04 - 40x40 paletted file, no interlacing +== s40n3p04.png s40_3p04.html diff --git a/image/test/reftest/pngsuite-oddsizes/s01_3p01.html b/image/test/reftest/pngsuite-oddsizes/s01_3p01.html new file mode 100644 index 0000000000..f38d98f1a6 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s01_3p01.html @@ -0,0 +1,9 @@ + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s01i3p01.png b/image/test/reftest/pngsuite-oddsizes/s01i3p01.png new file mode 100644 index 0000000000..6c0fad1fc9 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s01i3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s01n3p01.png b/image/test/reftest/pngsuite-oddsizes/s01n3p01.png new file mode 100644 index 0000000000..cb2c8c7826 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s01n3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s02_3p01.html b/image/test/reftest/pngsuite-oddsizes/s02_3p01.html new file mode 100644 index 0000000000..ad4660a24c --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s02_3p01.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s02i3p01.png b/image/test/reftest/pngsuite-oddsizes/s02i3p01.png new file mode 100644 index 0000000000..2defaed911 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s02i3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s02n3p01.png b/image/test/reftest/pngsuite-oddsizes/s02n3p01.png new file mode 100644 index 0000000000..2b1b669643 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s02n3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s03_3p01.html b/image/test/reftest/pngsuite-oddsizes/s03_3p01.html new file mode 100644 index 0000000000..adff34db31 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s03_3p01.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s03i3p01.png b/image/test/reftest/pngsuite-oddsizes/s03i3p01.png new file mode 100644 index 0000000000..c23fdc4631 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s03i3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s03n3p01.png b/image/test/reftest/pngsuite-oddsizes/s03n3p01.png new file mode 100644 index 0000000000..6d96ee4f87 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s03n3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s04_3p01.html b/image/test/reftest/pngsuite-oddsizes/s04_3p01.html new file mode 100644 index 0000000000..d97c2d42cc --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s04_3p01.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s04i3p01.png b/image/test/reftest/pngsuite-oddsizes/s04i3p01.png new file mode 100644 index 0000000000..0e710c2c39 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s04i3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s04n3p01.png b/image/test/reftest/pngsuite-oddsizes/s04n3p01.png new file mode 100644 index 0000000000..956396c45b Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s04n3p01.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s05_3p02.html b/image/test/reftest/pngsuite-oddsizes/s05_3p02.html new file mode 100644 index 0000000000..e5664fb9de --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s05_3p02.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s05i3p02.png b/image/test/reftest/pngsuite-oddsizes/s05i3p02.png new file mode 100644 index 0000000000..d14cbd351a Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s05i3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s05n3p02.png b/image/test/reftest/pngsuite-oddsizes/s05n3p02.png new file mode 100644 index 0000000000..bf940f0576 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s05n3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s06_3p02.html b/image/test/reftest/pngsuite-oddsizes/s06_3p02.html new file mode 100644 index 0000000000..6d13dc56fc --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s06_3p02.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s06i3p02.png b/image/test/reftest/pngsuite-oddsizes/s06i3p02.png new file mode 100644 index 0000000000..456ada3200 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s06i3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s06n3p02.png b/image/test/reftest/pngsuite-oddsizes/s06n3p02.png new file mode 100644 index 0000000000..501064dc25 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s06n3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s07_3p02.html b/image/test/reftest/pngsuite-oddsizes/s07_3p02.html new file mode 100644 index 0000000000..3fc2d42a71 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s07_3p02.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s07i3p02.png b/image/test/reftest/pngsuite-oddsizes/s07i3p02.png new file mode 100644 index 0000000000..44b66bab9e Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s07i3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s07n3p02.png b/image/test/reftest/pngsuite-oddsizes/s07n3p02.png new file mode 100644 index 0000000000..6a582593d6 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s07n3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s08_3p02.html b/image/test/reftest/pngsuite-oddsizes/s08_3p02.html new file mode 100644 index 0000000000..52079c3195 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s08_3p02.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s08i3p02.png b/image/test/reftest/pngsuite-oddsizes/s08i3p02.png new file mode 100644 index 0000000000..acf74f3fc4 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s08i3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s08n3p02.png b/image/test/reftest/pngsuite-oddsizes/s08n3p02.png new file mode 100644 index 0000000000..b7094e1b4f Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s08n3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s09_3p02.html b/image/test/reftest/pngsuite-oddsizes/s09_3p02.html new file mode 100644 index 0000000000..3b994e1283 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s09_3p02.html @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s09i3p02.png b/image/test/reftest/pngsuite-oddsizes/s09i3p02.png new file mode 100644 index 0000000000..0bfae8e456 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s09i3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s09n3p02.png b/image/test/reftest/pngsuite-oddsizes/s09n3p02.png new file mode 100644 index 0000000000..711ab82451 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s09n3p02.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s32_3p04.html b/image/test/reftest/pngsuite-oddsizes/s32_3p04.html new file mode 100644 index 0000000000..a10399ba62 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s32_3p04.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s32i3p04.png b/image/test/reftest/pngsuite-oddsizes/s32i3p04.png new file mode 100644 index 0000000000..0841910b72 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s32i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s32n3p04.png b/image/test/reftest/pngsuite-oddsizes/s32n3p04.png new file mode 100644 index 0000000000..fa58e3e3f6 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s32n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s33_3p04.html b/image/test/reftest/pngsuite-oddsizes/s33_3p04.html new file mode 100644 index 0000000000..d845558ceb --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s33_3p04.html @@ -0,0 +1,1161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s33i3p04.png b/image/test/reftest/pngsuite-oddsizes/s33i3p04.png new file mode 100644 index 0000000000..ab0dc14aba Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s33i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s33n3p04.png b/image/test/reftest/pngsuite-oddsizes/s33n3p04.png new file mode 100644 index 0000000000..764f1a3dc7 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s33n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s34_3p04.html b/image/test/reftest/pngsuite-oddsizes/s34_3p04.html new file mode 100644 index 0000000000..605ff92648 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s34_3p04.html @@ -0,0 +1,1230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s34i3p04.png b/image/test/reftest/pngsuite-oddsizes/s34i3p04.png new file mode 100644 index 0000000000..bd99039be4 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s34i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s34n3p04.png b/image/test/reftest/pngsuite-oddsizes/s34n3p04.png new file mode 100644 index 0000000000..9cbc68b3b9 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s34n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s35_3p04.html b/image/test/reftest/pngsuite-oddsizes/s35_3p04.html new file mode 100644 index 0000000000..6a5f720a99 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s35_3p04.html @@ -0,0 +1,1301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s35i3p04.png b/image/test/reftest/pngsuite-oddsizes/s35i3p04.png new file mode 100644 index 0000000000..e2a5e0a659 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s35i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s35n3p04.png b/image/test/reftest/pngsuite-oddsizes/s35n3p04.png new file mode 100644 index 0000000000..90b892ebaf Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s35n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s36_3p04.html b/image/test/reftest/pngsuite-oddsizes/s36_3p04.html new file mode 100644 index 0000000000..68a5ae4be1 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s36_3p04.html @@ -0,0 +1,1374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s36i3p04.png b/image/test/reftest/pngsuite-oddsizes/s36i3p04.png new file mode 100644 index 0000000000..eb61b6f9a3 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s36i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s36n3p04.png b/image/test/reftest/pngsuite-oddsizes/s36n3p04.png new file mode 100644 index 0000000000..b38d179774 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s36n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s37_3p04.html b/image/test/reftest/pngsuite-oddsizes/s37_3p04.html new file mode 100644 index 0000000000..0f19b653a1 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s37_3p04.html @@ -0,0 +1,1449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s37i3p04.png b/image/test/reftest/pngsuite-oddsizes/s37i3p04.png new file mode 100644 index 0000000000..6e2b1e9b79 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s37i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s37n3p04.png b/image/test/reftest/pngsuite-oddsizes/s37n3p04.png new file mode 100644 index 0000000000..4d3054da51 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s37n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s38_3p04.html b/image/test/reftest/pngsuite-oddsizes/s38_3p04.html new file mode 100644 index 0000000000..38a8692988 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s38_3p04.html @@ -0,0 +1,1526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s38i3p04.png b/image/test/reftest/pngsuite-oddsizes/s38i3p04.png new file mode 100644 index 0000000000..a0a8a140ad Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s38i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s38n3p04.png b/image/test/reftest/pngsuite-oddsizes/s38n3p04.png new file mode 100644 index 0000000000..1233ed048e Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s38n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s39_3p04.html b/image/test/reftest/pngsuite-oddsizes/s39_3p04.html new file mode 100644 index 0000000000..6a00026dc0 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s39_3p04.html @@ -0,0 +1,1605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s39i3p04.png b/image/test/reftest/pngsuite-oddsizes/s39i3p04.png new file mode 100644 index 0000000000..04fee93eae Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s39i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s39n3p04.png b/image/test/reftest/pngsuite-oddsizes/s39n3p04.png new file mode 100644 index 0000000000..c750100d55 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s39n3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s40_3p04.html b/image/test/reftest/pngsuite-oddsizes/s40_3p04.html new file mode 100644 index 0000000000..59b18da4d7 --- /dev/null +++ b/image/test/reftest/pngsuite-oddsizes/s40_3p04.html @@ -0,0 +1,1686 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-oddsizes/s40i3p04.png b/image/test/reftest/pngsuite-oddsizes/s40i3p04.png new file mode 100644 index 0000000000..68f358b822 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s40i3p04.png differ diff --git a/image/test/reftest/pngsuite-oddsizes/s40n3p04.png b/image/test/reftest/pngsuite-oddsizes/s40n3p04.png new file mode 100644 index 0000000000..864b6b9673 Binary files /dev/null and b/image/test/reftest/pngsuite-oddsizes/s40n3p04.png differ diff --git a/image/test/reftest/pngsuite-palettes/pp0n2c16.html b/image/test/reftest/pngsuite-palettes/pp0n2c16.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/pp0n2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-palettes/pp0n2c16.png b/image/test/reftest/pngsuite-palettes/pp0n2c16.png new file mode 100644 index 0000000000..8f2aad7335 Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/pp0n2c16.png differ diff --git a/image/test/reftest/pngsuite-palettes/pp0n6a08.png b/image/test/reftest/pngsuite-palettes/pp0n6a08.png new file mode 100644 index 0000000000..4ed7a30e4d Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/pp0n6a08.png differ diff --git a/image/test/reftest/pngsuite-palettes/ps1n0g08.html b/image/test/reftest/pngsuite-palettes/ps1n0g08.html new file mode 100644 index 0000000000..5aaf11cabb --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/ps1n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-palettes/ps1n0g08.png b/image/test/reftest/pngsuite-palettes/ps1n0g08.png new file mode 100644 index 0000000000..2053df2ba3 Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/ps1n0g08.png differ diff --git a/image/test/reftest/pngsuite-palettes/ps1n2c16.html b/image/test/reftest/pngsuite-palettes/ps1n2c16.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/ps1n2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-palettes/ps1n2c16.png b/image/test/reftest/pngsuite-palettes/ps1n2c16.png new file mode 100644 index 0000000000..b03ecfc669 Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/ps1n2c16.png differ diff --git a/image/test/reftest/pngsuite-palettes/ps2n0g08.html b/image/test/reftest/pngsuite-palettes/ps2n0g08.html new file mode 100644 index 0000000000..5aaf11cabb --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/ps2n0g08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-palettes/ps2n0g08.png b/image/test/reftest/pngsuite-palettes/ps2n0g08.png new file mode 100644 index 0000000000..beeab8ff3d Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/ps2n0g08.png differ diff --git a/image/test/reftest/pngsuite-palettes/ps2n2c16.html b/image/test/reftest/pngsuite-palettes/ps2n2c16.html new file mode 100644 index 0000000000..dd08f0e3d0 --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/ps2n2c16.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-palettes/ps2n2c16.png b/image/test/reftest/pngsuite-palettes/ps2n2c16.png new file mode 100644 index 0000000000..c256f9091b Binary files /dev/null and b/image/test/reftest/pngsuite-palettes/ps2n2c16.png differ diff --git a/image/test/reftest/pngsuite-palettes/reftest.list b/image/test/reftest/pngsuite-palettes/reftest.list new file mode 100644 index 0000000000..56eb56e0a2 --- /dev/null +++ b/image/test/reftest/pngsuite-palettes/reftest.list @@ -0,0 +1,14 @@ +# PngSuite - Additional palettes + +# pp0n2c16 - six-cube palette-chunk in true-color image +== pp0n2c16.png pp0n2c16.html +# pp0n6a08 - six-cube palette-chunk in true-color+alpha image +#== pp0n6a08.png pp0n6a08.html +# ps1n0g08 - six-cube suggested palette (1 byte) in grayscale image +== ps1n0g08.png ps1n0g08.html +# ps1n2c16 - six-cube suggested palette (1 byte) in true-color image +== ps1n2c16.png ps1n2c16.html +# ps2n0g08 - six-cube suggested palette (2 bytes) in grayscale image +== ps2n0g08.png ps2n0g08.html +# ps2n2c16 - six-cube suggested palette (2 bytes) in true-color image +== ps2n2c16.png ps2n2c16.html diff --git a/image/test/reftest/pngsuite-zlib/reftest.list b/image/test/reftest/pngsuite-zlib/reftest.list new file mode 100644 index 0000000000..ec153449fb --- /dev/null +++ b/image/test/reftest/pngsuite-zlib/reftest.list @@ -0,0 +1,8 @@ +# z00n2c08 - color, no interlacing, compression level 0 (none) +== z00n2c08.png z00n2c08.html +# z03n2c08 - color, no interlacing, compression level 3 +== z03n2c08.png z03n2c08.html +# z06n2c08 - color, no interlacing, compression level 6 (default) +== z06n2c08.png z06n2c08.html +# z09n2c08 - color, no interlacing, compression level 9 (maximum) +== z09n2c08.png z09n2c08.html diff --git a/image/test/reftest/pngsuite-zlib/z00n2c08.html b/image/test/reftest/pngsuite-zlib/z00n2c08.html new file mode 100644 index 0000000000..c878a03ff6 --- /dev/null +++ b/image/test/reftest/pngsuite-zlib/z00n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-zlib/z00n2c08.png b/image/test/reftest/pngsuite-zlib/z00n2c08.png new file mode 100644 index 0000000000..7669eb8385 Binary files /dev/null and b/image/test/reftest/pngsuite-zlib/z00n2c08.png differ diff --git a/image/test/reftest/pngsuite-zlib/z03n2c08.html b/image/test/reftest/pngsuite-zlib/z03n2c08.html new file mode 100644 index 0000000000..c878a03ff6 --- /dev/null +++ b/image/test/reftest/pngsuite-zlib/z03n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-zlib/z03n2c08.png b/image/test/reftest/pngsuite-zlib/z03n2c08.png new file mode 100644 index 0000000000..bfb10de8de Binary files /dev/null and b/image/test/reftest/pngsuite-zlib/z03n2c08.png differ diff --git a/image/test/reftest/pngsuite-zlib/z06n2c08.html b/image/test/reftest/pngsuite-zlib/z06n2c08.html new file mode 100644 index 0000000000..c878a03ff6 --- /dev/null +++ b/image/test/reftest/pngsuite-zlib/z06n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-zlib/z06n2c08.png b/image/test/reftest/pngsuite-zlib/z06n2c08.png new file mode 100644 index 0000000000..b90ebc10f5 Binary files /dev/null and b/image/test/reftest/pngsuite-zlib/z06n2c08.png differ diff --git a/image/test/reftest/pngsuite-zlib/z09n2c08.html b/image/test/reftest/pngsuite-zlib/z09n2c08.html new file mode 100644 index 0000000000..c878a03ff6 --- /dev/null +++ b/image/test/reftest/pngsuite-zlib/z09n2c08.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/image/test/reftest/pngsuite-zlib/z09n2c08.png b/image/test/reftest/pngsuite-zlib/z09n2c08.png new file mode 100644 index 0000000000..5f191a78ee Binary files /dev/null and b/image/test/reftest/pngsuite-zlib/z09n2c08.png differ diff --git a/image/test/reftest/reftest.list b/image/test/reftest/reftest.list new file mode 100644 index 0000000000..109a0676e1 --- /dev/null +++ b/image/test/reftest/reftest.list @@ -0,0 +1,55 @@ +# Check for 24-bit color mode (test for bug 414720) +== colordepth.html about:blank + +# "PngSuite, the official set of PNG test images" +# Images by Willem van Schaik +# +# http://www.schaik.com/pngsuite/pngsuite.html +# http://www.libpng.org/pub/png/pngsuite.html +include pngsuite-basic-n/reftest.list +include pngsuite-basic-i/reftest.list +include pngsuite-ancillary/reftest.list +include pngsuite-background/reftest.list +include pngsuite-chunkorder/reftest.list +include pngsuite-corrupted/reftest.list +include pngsuite-filtering/reftest.list +include pngsuite-gamma/reftest.list +include pngsuite-oddsizes/reftest.list +include pngsuite-palettes/reftest.list +include pngsuite-zlib/reftest.list + +# BMP tests +skip-if(Android) include bmp/reftest.list + +# ICO tests +skip-if(Android) include ico/reftest.list + +# JPEG tests +include jpeg/reftest.list + +# JXL tests +skip-if(Android||!jxl) include jxl/reftest.list + +# GIF tests +include gif/reftest.list + +# APNG tests +include apng/reftest.list + +# AVIF tests +include avif/reftest.list + +# Generic image tests +include generic/reftest.list + +# Color management test +include color-management/reftest.list + +# Downscaling tests +skip-if(useDrawSnapshot) include downscaling/reftest.list + +# Lossless encoders +include encoders-lossless/reftest.list + +# webp tests +include webp/reftest.list diff --git a/image/test/reftest/webp/blue.png b/image/test/reftest/webp/blue.png new file mode 100644 index 0000000000..7b62530006 Binary files /dev/null and b/image/test/reftest/webp/blue.png differ diff --git a/image/test/reftest/webp/icc-bit-no-icc-chunk.webp b/image/test/reftest/webp/icc-bit-no-icc-chunk.webp new file mode 100644 index 0000000000..66d38f46fe Binary files /dev/null and b/image/test/reftest/webp/icc-bit-no-icc-chunk.webp differ diff --git a/image/test/reftest/webp/reftest.list b/image/test/reftest/webp/reftest.list new file mode 100644 index 0000000000..971d895a4d --- /dev/null +++ b/image/test/reftest/webp/reftest.list @@ -0,0 +1 @@ +== icc-bit-no-icc-chunk.webp blue.png diff --git a/image/test/unit/async_load_tests.js b/image/test/unit/async_load_tests.js new file mode 100644 index 0000000000..06792349ee --- /dev/null +++ b/image/test/unit/async_load_tests.js @@ -0,0 +1,298 @@ +/* + * Test to ensure that image loading/decoding notifications are always + * delivered async, and in the order we expect. + * + * Must be included from a file that has a uri of the image to test defined in + * var uri. + */ +/* import-globals-from image_load_helpers.js */ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +var server = new HttpServer(); +server.registerDirectory("/", do_get_file("")); +server.registerContentType("sjs", "sjs"); +server.start(-1); + +load("image_load_helpers.js"); + +var requests = []; +/* global uri */ + +// Return a closure that holds on to the listener from the original +// imgIRequest, and compares its results to the cloned one. +function getCloneStopCallback(original_listener) { + return function cloneStop(listener) { + Assert.equal(original_listener.state, listener.state); + + // Sanity check to make sure we didn't accidentally use the same listener + // twice. + Assert.notEqual(original_listener, listener); + do_test_finished(); + }; +} + +// Make sure that cloned requests get all the same callbacks as the original, +// but they aren't synchronous right now. +function checkClone(other_listener, aRequest) { + do_test_pending(); + + // For as long as clone notification is synchronous, we can't test the clone state reliably. + var listener = new ImageListener( + null, + function (foo, bar) { + do_test_finished(); + } /* getCloneStopCallback(other_listener)*/ + ); + listener.synchronous = false; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var clone = aRequest.clone(outer); + requests.push({ request: clone, locked: false }); +} + +// Ensure that all the callbacks were called on aRequest. +function checkSizeAndLoad(listener, aRequest) { + Assert.notEqual(listener.state & SIZE_AVAILABLE, 0); + Assert.notEqual(listener.state & LOAD_COMPLETE, 0); + + do_test_finished(); +} + +function secondLoadDone(oldlistener, aRequest) { + do_test_pending(); + + try { + var staticrequest = aRequest.getStaticRequest(); + + // For as long as clone notification is synchronous, we can't test the + // clone state reliably. + var listener = new ImageListener(null, checkSizeAndLoad); + listener.synchronous = false; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var staticrequestclone = staticrequest.clone(outer); + requests.push({ request: staticrequestclone, locked: false }); + } catch (e) { + // We can't create a static request. Most likely the request we started + // with didn't load successfully. + do_test_finished(); + } + + run_loadImageWithChannel_tests(); + + do_test_finished(); +} + +// Load the request a second time. This should come from the image cache, and +// therefore would be at most risk of being served synchronously. +function checkSecondLoad() { + do_test_pending(); + + var listener = new ImageListener(checkClone, secondLoadDone); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push({ + request: gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ), + locked: false, + }); + listener.synchronous = false; +} + +function firstLoadDone(oldlistener, aRequest) { + checkSecondLoad(uri); + + do_test_finished(); +} + +// Return a closure that allows us to check the stream listener's status when the +// image finishes loading. +function getChannelLoadImageStopCallback(streamlistener, next) { + return function channelLoadStop(imglistener, aRequest) { + next(); + + do_test_finished(); + }; +} + +// Load the request a second time. This should come from the image cache, and +// therefore would be at most risk of being served synchronously. +function checkSecondChannelLoad() { + do_test_pending(); + var channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var channellistener = new ChannelListener(); + channel.asyncOpen(channellistener); + + var listener = new ImageListener( + null, + getChannelLoadImageStopCallback(channellistener, all_done_callback) + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var outlistener = {}; + requests.push({ + request: gCurrentLoader.loadImageWithChannelXPCOM( + channel, + outer, + null, + outlistener + ), + locked: false, + }); + channellistener.outputListener = outlistener.value; + + listener.synchronous = false; +} + +function run_loadImageWithChannel_tests() { + // To ensure we're testing what we expect to, create a new loader and cache. + gCurrentLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader + ); + + do_test_pending(); + var channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var channellistener = new ChannelListener(); + channel.asyncOpen(channellistener); + + var listener = new ImageListener( + null, + getChannelLoadImageStopCallback(channellistener, checkSecondChannelLoad) + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var outlistener = {}; + requests.push({ + request: gCurrentLoader.loadImageWithChannelXPCOM( + channel, + outer, + null, + outlistener + ), + locked: false, + }); + channellistener.outputListener = outlistener.value; + + listener.synchronous = false; +} + +function all_done_callback() { + server.stop(function () { + do_test_finished(); + }); +} + +function startImageCallback(otherCb) { + return function (listener, request) { + // Make sure we can load the same image immediately out of the cache. + do_test_pending(); + var listener2 = new ImageListener(null, function (foo, bar) { + do_test_finished(); + }); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener2); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push({ + request: gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ), + locked: false, + }); + listener2.synchronous = false; + + // Now that we've started another load, chain to the callback. + otherCb(listener, request); + }; +} + +var gCurrentLoader; + +function cleanup() { + for (let { request, locked } of requests) { + if (locked) { + try { + request.unlockImage(); + } catch (e) {} + } + request.cancelAndForgetObserver(0); + } +} + +function run_test() { + registerCleanupFunction(cleanup); + + gCurrentLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader + ); + + do_test_pending(); + var listener = new ImageListener( + startImageCallback(checkClone), + firstLoadDone + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + var req = gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ); + + // Ensure that we don't cause any mayhem when we lock an image. + req.lockImage(); + + requests.push({ request: req, locked: true }); + + listener.synchronous = false; +} diff --git a/image/test/unit/bug413512.ico b/image/test/unit/bug413512.ico new file mode 100644 index 0000000000..b2db0429f6 Binary files /dev/null and b/image/test/unit/bug413512.ico differ diff --git a/image/test/unit/bug815359.ico b/image/test/unit/bug815359.ico new file mode 100644 index 0000000000..a24b8fb6bb Binary files /dev/null and b/image/test/unit/bug815359.ico differ diff --git a/image/test/unit/image1.png b/image/test/unit/image1.png new file mode 100644 index 0000000000..2fb37aeec4 Binary files /dev/null and b/image/test/unit/image1.png differ diff --git a/image/test/unit/image1.webp b/image/test/unit/image1.webp new file mode 100644 index 0000000000..b2a6f92aaa Binary files /dev/null and b/image/test/unit/image1.webp differ diff --git a/image/test/unit/image1png16x16.jpg b/image/test/unit/image1png16x16.jpg new file mode 100644 index 0000000000..488b563c90 Binary files /dev/null and b/image/test/unit/image1png16x16.jpg differ diff --git a/image/test/unit/image1png64x64.jpg b/image/test/unit/image1png64x64.jpg new file mode 100644 index 0000000000..679dad2b95 Binary files /dev/null and b/image/test/unit/image1png64x64.jpg differ diff --git a/image/test/unit/image1quality50.webp b/image/test/unit/image1quality50.webp new file mode 100644 index 0000000000..f73d615657 Binary files /dev/null and b/image/test/unit/image1quality50.webp differ diff --git a/image/test/unit/image2.jpg b/image/test/unit/image2.jpg new file mode 100644 index 0000000000..b2131bf0c1 Binary files /dev/null and b/image/test/unit/image2.jpg differ diff --git a/image/test/unit/image2jpg16x16-win.png b/image/test/unit/image2jpg16x16-win.png new file mode 100644 index 0000000000..a821626c07 Binary files /dev/null and b/image/test/unit/image2jpg16x16-win.png differ diff --git a/image/test/unit/image2jpg16x16.png b/image/test/unit/image2jpg16x16.png new file mode 100644 index 0000000000..b5b9a720a8 Binary files /dev/null and b/image/test/unit/image2jpg16x16.png differ diff --git a/image/test/unit/image2jpg16x16cropped.jpg b/image/test/unit/image2jpg16x16cropped.jpg new file mode 100644 index 0000000000..fca22cb30a Binary files /dev/null and b/image/test/unit/image2jpg16x16cropped.jpg differ diff --git a/image/test/unit/image2jpg16x16cropped2.jpg b/image/test/unit/image2jpg16x16cropped2.jpg new file mode 100644 index 0000000000..e51d3530d3 Binary files /dev/null and b/image/test/unit/image2jpg16x16cropped2.jpg differ diff --git a/image/test/unit/image2jpg16x32cropped3.jpg b/image/test/unit/image2jpg16x32cropped3.jpg new file mode 100644 index 0000000000..13a3d26e54 Binary files /dev/null and b/image/test/unit/image2jpg16x32cropped3.jpg differ diff --git a/image/test/unit/image2jpg16x32scaled.jpg b/image/test/unit/image2jpg16x32scaled.jpg new file mode 100644 index 0000000000..6abef0f99b Binary files /dev/null and b/image/test/unit/image2jpg16x32scaled.jpg differ diff --git a/image/test/unit/image2jpg32x16cropped4.jpg b/image/test/unit/image2jpg32x16cropped4.jpg new file mode 100644 index 0000000000..46f34918c8 Binary files /dev/null and b/image/test/unit/image2jpg32x16cropped4.jpg differ diff --git a/image/test/unit/image2jpg32x16scaled.jpg b/image/test/unit/image2jpg32x16scaled.jpg new file mode 100644 index 0000000000..e302fbafd0 Binary files /dev/null and b/image/test/unit/image2jpg32x16scaled.jpg differ diff --git a/image/test/unit/image2jpg32x32-win.png b/image/test/unit/image2jpg32x32-win.png new file mode 100644 index 0000000000..4d84df26a0 Binary files /dev/null and b/image/test/unit/image2jpg32x32-win.png differ diff --git a/image/test/unit/image2jpg32x32.jpg b/image/test/unit/image2jpg32x32.jpg new file mode 100644 index 0000000000..cf9a10a37f Binary files /dev/null and b/image/test/unit/image2jpg32x32.jpg differ diff --git a/image/test/unit/image2jpg32x32.png b/image/test/unit/image2jpg32x32.png new file mode 100644 index 0000000000..42640cbb53 Binary files /dev/null and b/image/test/unit/image2jpg32x32.png differ diff --git a/image/test/unit/image3.ico b/image/test/unit/image3.ico new file mode 100644 index 0000000000..d44438903b Binary files /dev/null and b/image/test/unit/image3.ico differ diff --git a/image/test/unit/image3ico16x16.png b/image/test/unit/image3ico16x16.png new file mode 100644 index 0000000000..fa61cc5046 Binary files /dev/null and b/image/test/unit/image3ico16x16.png differ diff --git a/image/test/unit/image3ico32x32.png b/image/test/unit/image3ico32x32.png new file mode 100644 index 0000000000..58a72e5c9d Binary files /dev/null and b/image/test/unit/image3ico32x32.png differ diff --git a/image/test/unit/image4.gif b/image/test/unit/image4.gif new file mode 100644 index 0000000000..b1530bc81e Binary files /dev/null and b/image/test/unit/image4.gif differ diff --git a/image/test/unit/image4gif16x16bmp24bpp.ico b/image/test/unit/image4gif16x16bmp24bpp.ico new file mode 100644 index 0000000000..890c81c272 Binary files /dev/null and b/image/test/unit/image4gif16x16bmp24bpp.ico differ diff --git a/image/test/unit/image4gif16x16bmp32bpp.ico b/image/test/unit/image4gif16x16bmp32bpp.ico new file mode 100644 index 0000000000..f8a9eb8adc Binary files /dev/null and b/image/test/unit/image4gif16x16bmp32bpp.ico differ diff --git a/image/test/unit/image4gif32x32bmp24bpp.ico b/image/test/unit/image4gif32x32bmp24bpp.ico new file mode 100644 index 0000000000..28092818dc Binary files /dev/null and b/image/test/unit/image4gif32x32bmp24bpp.ico differ diff --git a/image/test/unit/image4gif32x32bmp32bpp.ico b/image/test/unit/image4gif32x32bmp32bpp.ico new file mode 100644 index 0000000000..0e2d28c82a Binary files /dev/null and b/image/test/unit/image4gif32x32bmp32bpp.ico differ diff --git a/image/test/unit/image_load_helpers.js b/image/test/unit/image_load_helpers.js new file mode 100644 index 0000000000..6d1e605bf5 --- /dev/null +++ b/image/test/unit/image_load_helpers.js @@ -0,0 +1,124 @@ +/* + * Helper structures to track callbacks from image and channel loads. + */ + +// START_REQUEST and STOP_REQUEST are used by ChannelListener, and +// stored in ChannelListener.requestStatus. +const START_REQUEST = 0x01; +const STOP_REQUEST = 0x02; +const DATA_AVAILABLE = 0x04; + +// One bit per callback that imageListener below implements. Stored in +// ImageListener.state. +const SIZE_AVAILABLE = 0x01; +const FRAME_UPDATE = 0x02; +const FRAME_COMPLETE = 0x04; +const LOAD_COMPLETE = 0x08; +const DECODE_COMPLETE = 0x10; + +// Safebrowsing requires that the profile dir is set. +do_get_profile(); + +// An implementation of imgIScriptedNotificationObserver with the ability to +// call specified functions on onStartRequest and onStopRequest. +function ImageListener(start_callback, stop_callback) { + this.sizeAvailable = function onSizeAvailable(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= SIZE_AVAILABLE; + + if (this.start_callback) { + this.start_callback(this, aRequest); + } + }; + this.frameComplete = function onFrameComplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= FRAME_COMPLETE; + }; + this.decodeComplete = function onDecodeComplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= DECODE_COMPLETE; + }; + this.loadComplete = function onLoadcomplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= LOAD_COMPLETE; + + if (this.stop_callback) { + this.stop_callback(this, aRequest); + } + }; + this.frameUpdate = function onFrameUpdate(aRequest) {}; + this.isAnimated = function onIsAnimated() {}; + + // Initialize the synchronous flag to true to start. This must be set to + // false before exiting to the event loop! + this.synchronous = true; + + // A function to call when onStartRequest is called. + this.start_callback = start_callback; + + // A function to call when onStopRequest is called. + this.stop_callback = stop_callback; + + // The image load/decode state. + // A bitfield that tracks which callbacks have been called. Takes the bits + // defined above. + this.state = 0; +} + +function NS_FAILED(val) { + return !!(val & 0x80000000); +} + +function ChannelListener() { + this.onStartRequest = function onStartRequest(aRequest) { + if (this.outputListener) { + this.outputListener.onStartRequest(aRequest); + } + + this.requestStatus |= START_REQUEST; + }; + + this.onDataAvailable = function onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ) { + if (this.outputListener) { + this.outputListener.onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ); + } + + this.requestStatus |= DATA_AVAILABLE; + }; + + this.onStopRequest = function onStopRequest(aRequest, aStatusCode) { + if (this.outputListener) { + this.outputListener.onStopRequest(aRequest, aStatusCode); + } + + // If we failed (or were canceled - failure is implied if canceled), + // there's no use tracking our state, since it's meaningless. + if (NS_FAILED(aStatusCode)) { + this.requestStatus = 0; + } else { + this.requestStatus |= STOP_REQUEST; + } + }; + + // A listener to pass the notifications we get to. + this.outputListener = null; + + // The request's status. A bitfield that holds one or both of START_REQUEST + // and STOP_REQUEST, according to which callbacks have been called on the + // associated request. + this.requestStatus = 0; +} diff --git a/image/test/unit/test_async_notification.js b/image/test/unit/test_async_notification.js new file mode 100644 index 0000000000..3f5f47c271 --- /dev/null +++ b/image/test/unit/test_async_notification.js @@ -0,0 +1,15 @@ +/* + * Test for asynchronous image load/decode notifications in the case that the image load works. + */ + +// A simple 3x3 png; rows go red, green, blue. Stolen from the PNG encoder test. + +var pngspec = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="; +var ioService = Services.io; + +// This is used in async_load_tests.js +/* exported uri */ +var uri = ioService.newURI(pngspec); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_async_notification_404.js b/image/test/unit/test_async_notification_404.js new file mode 100644 index 0000000000..c8aab9fe1d --- /dev/null +++ b/image/test/unit/test_async_notification_404.js @@ -0,0 +1,23 @@ +/* + * Test to ensure that load/decode notifications are delivered completely and + * asynchronously when dealing with a file that's a 404. + */ +/* import-globals-from async_load_tests.js */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var ioService = Services.io; + +// This is used in async_load_tests.js +// eslint-disable-next-line no-unused-vars +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return ioService.newURI( + "http://localhost:" + + server.identity.primaryPort + + "/async-notification-never-here.jpg" + ); +}); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_async_notification_animated.js b/image/test/unit/test_async_notification_animated.js new file mode 100644 index 0000000000..f201f90f60 --- /dev/null +++ b/image/test/unit/test_async_notification_animated.js @@ -0,0 +1,19 @@ +/* + * Test for asynchronous image load/decode notifications in the case that the + * image load works, but for an animated image. + * + * If this fails because a request wasn't cancelled, it's possible that + * imgContainer::ExtractFrame didn't set the new image's status correctly. + */ + +// transparent-animation.gif from the gif reftests. + +var spec = + "data:image/gif;base64,R0lGODlhZABkAIABAP8AAP///yH5BAkBAAEALAAAAABLAGQAAAK8jI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8YhMKpchgPMJjUqnVOipis1ir9qul+sNV8HistVkTj/JajG7/UXDy+95tm4fy/NdPF/q93dWIqgVWAhwWKgoyPjnyAeZJ2lHOWcJh9mmqcaZ5mkGSreHOCXqRloadRrGGkeoapoa6+TaN0tra4gbq3vHq+q7BVwqrMeEnKy8zNzs/AwdLT1NXW19jZ1tUgAAIfkECQEAAQAsAAAAADQAZAAAArCMj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/7vAAiHxKLxiCRCkswmc+mMSqHSapJqzSof2u4Q67WCw1MuOTs+N9Pqq7kdZcON8vk2aF+/88g6358HaCc4Rwhn2IaopnjGSOYYBukl2UWpZYm2x0enuXnX4NnXGQqAKTYaalqlWoZH+snwWsQah+pJ64Sr5ypbCvQLHCw8TFxsfIycrLzM3PxQAAAh+QQJAQABACwAAAAAGwBkAAACUIyPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gTE8kzX9o3n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvdrfYnH5LL5jE6r16sCADs="; +var ioService = Services.io; + +// This is used in async_load_tests.js +/* exported uri */ +var uri = ioService.newURI(spec); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_encoder_apng.js b/image/test/unit/test_encoder_apng.js new file mode 100644 index 0000000000..f055a0335f --- /dev/null +++ b/image/test/unit/test_encoder_apng.js @@ -0,0 +1,582 @@ +/* + * Test for APNG encoding in ImageLib + * + */ + +// dispose=[none|background|previous] +// blend=[source|over] + +var apng1A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGB format. + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, + 0, 0, 255, 0, 0, 255, 0, 0, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, + 255, 0, 0, 255, 0, 0, 255, 0, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAAA9JREFUCFtj/M8ABYxYWAA5IQMBD9nE1QAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAADuZcrMAAAAFGZkQVQAAAACCFtjZPjPAAGMWFgANiQDAVBdoI8AAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABVmZEFUAAAABAhbY2Rg+M8ABoxYWAAzJwMBWk5KPwAAAABJRU5ErkJggg==", +}; + +var apng1B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, + 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAADuZcrMAAAAFWZkQVQAAAACCFtjZPgPhFDAiJMDAInrBf4Q0nfOAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gAAAPzGSUAAAAWZmRBVAAAAAQIW2NkYPj/nwEKGHFyAIbuBf50PCpiAAAAAElFTkSuQmCC", +}; + +var apng1C = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // The first frame is skipped, so it will only flash green/blue (or static red in an APNG-unaware viewer) + width: 3, + height: 3, + skipFirstFrame: true, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, + 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAACAAAAAPONk3AAAAASSURBVAhbY/zPwABEEMCIkwMAjOgF/rfl9xUAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABVmZEFUAAAAAQhbY2T4D4RQwIiTAwCJ6wX++lSqrAAAABpmY1RMAAAAAgAAAAMAAAADAAAAAAAAAAAB9APoAACYgPPxAAAAFmZkQVQAAAADCFtjZGD4/58BChhxcgCG7gX+PgKhKQAAAABJRU5ErkJggg==", +}; + +var apng2A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over mode + // (The green frame is a horizontal gradient, and the blue frame is a + // vertical gradient. They stack as they animate.) + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, + 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 180, 0, 0, 255, + 180, 0, 0, 255, 180, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAAGZYvpaAAAAGWZkQVQAAAACCFtjZPgPhAwMW4F4OiNODgDI3wnis0vjTAAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAAF09CmzAAAAHGZkQVQAAAAECFtjZGD4780ABYxAzhZkzn8YBwBn4AT/ernr+wAAAABJRU5ErkJggg==", +}; + +var apng2B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over, dispose = background + // (The green frame is a horizontal gradient, and the blue frame is a + // vertical gradient. Each frame is displayed individually, blended to + // whatever the background is.) + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, + 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 180, 0, 0, 255, + 180, 0, 0, 255, 180, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AEAbA0RWQAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAQGAecsbAAAAGWZkQVQAAAACCFtjZPgPhAwMW4F4OiNODgDI3wnis0vjTAAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAQFt7xjyAAAAHGZkQVQAAAAECFtjZGD4780ABYxAzhZkzn8YBwBn4AT/ernr+wAAAABJRU5ErkJggg==", +}; + +var apng3 = { + // A 3x3 image with 4 frames. First frame is white, then 1x1 frames draw a diagonal line + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + ], + }, + + { + // frame #2 + width: 1, + height: 1, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + + { + // frame #3 + width: 1, + height: 1, + x_offset: 1, + y_offset: 1, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + + { + // frame #4 + width: 1, + height: 1, + x_offset: 2, + y_offset: 2, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + ], + + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAAEAAAAAHzNZtAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCFtj/A8EDFDAiJMDABlqC/jamhxvAAAAGmZjVEwAAAABAAAAAQAAAAEAAAAAAAAAAAH0A+gAADJXfawAAAARZmRBVAAAAAIIW2NgYGD4DwABBAEA0iEgKQAAABpmY1RMAAAAAwAAAAEAAAABAAAAAQAAAAEB9APoAAC4OHoxAAAAEWZkQVQAAAAECFtjYGBg+A8AAQQBACrja58AAAAaZmNUTAAAAAUAAAABAAAAAQAAAAIAAAACAfQD6AAA/fh01wAAABFmZEFUAAAABghbY2BgYPgPAAEEAQDLja8yAAAAAElFTkSuQmCC", +}; + +// Main test entry point. +function run_test() { + dump("Checking apng1A...\n"); + run_test_for(apng1A); + dump("Checking apng1B...\n"); + run_test_for(apng1B); + dump("Checking apng1C...\n"); + run_test_for(apng1C); + + dump("Checking apng2A...\n"); + run_test_for(apng2A); + dump("Checking apng2B...\n"); + run_test_for(apng2B); + + dump("Checking apng3...\n"); + run_test_for(apng3); +} + +function run_test_for(input) { + var encoder, dataURL; + + encoder = encodeImage(input); + dataURL = makeDataURL(encoder, "image/png"); + Assert.equal(dataURL, input.expected); +} + +function encodeImage(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + options += ";frames=" + input.frames.length; + options += ";skipfirstframe=" + (input.skipFirstFrame ? "yes" : "no"); + options += ";plays=" + input.plays; + encoder.startImageEncode(input.width, input.height, input.format, options); + + for (var i = 0; i < input.frames.length; i++) { + var frame = input.frames[i]; + + options = ""; + if (frame.transparency) { + options += "transparency=" + input.transparency; + } + options += ";delay=" + frame.delay; + options += ";dispose=" + frame.dispose; + options += ";blend=" + frame.blend; + if (frame.x_offset > 0) { + options += ";xoffset=" + frame.x_offset; + } + if (frame.y_offset > 0) { + options += ";yoffset=" + frame.y_offset; + } + + encoder.addImageFrame( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + } + + encoder.endImageEncode(); + + return encoder; +} + +function makeDataURL(encoder, mimetype) { + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); + + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + stream.QueryInterface(Ci.nsIBinaryInputStream); + + stream.setInputStream(rawStream); + + var bytes = stream.readByteArray(stream.available()); // returns int[] + + var base64String = toBase64(bytes); + + return "data:" + mimetype + ";base64," + base64String; +} + +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = + // eslint-disable-next-line no-useless-concat + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +const base64Pad = "="; +function toBase64(data) { + var result = ""; + var length = data.length; + var i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < length - 2; i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length % 3) { + i = length - (length % 3); + result += toBase64Table[data[i] >> 2]; + if (length % 3 == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[(data[i + 1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +} diff --git a/image/test/unit/test_encoder_png.js b/image/test/unit/test_encoder_png.js new file mode 100644 index 0000000000..73e91f573f --- /dev/null +++ b/image/test/unit/test_encoder_png.js @@ -0,0 +1,263 @@ +/* + * Test for PNG encoding in ImageLib + * + */ + +var png1A = { + // A 3x3 image, rows are red, green, blue. + // RGB format, transparency defaults. + + transparency: null, + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +var png1B = { + // A 3x3 image, rows are red, green, blue. + // RGB format, transparency=none. + + transparency: "none", + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +var png2A = { + // A 3x3 image, rows are: red, green, blue. Columns are: 0%, 33%, 66% transparent. + + transparency: null, + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 170, 255, 0, 0, 85, 0, 255, 0, 255, 0, 255, + 0, 170, 0, 255, 0, 85, 0, 0, 255, 255, 0, 0, 255, 170, 0, 0, 255, 85, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAGUlEQVQIW2P8z8AARAyrQZgRyETiMPyHcwDKCwoAGxxLEQAAAABJRU5ErkJggg==", +}; + +var png2B = { + // A 3x3 image, rows are: red, green, blue. Columns are: 0%, 33%, 66% transparent, + // but transparency will be ignored. + + transparency: "none", + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 170, 255, 0, 0, 85, 0, 255, 0, 255, 0, 255, + 0, 170, 0, 255, 0, 85, 0, 0, 255, 255, 0, 0, 255, 170, 0, 0, 255, 85, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +// Main test entry point. +function run_test() { + dump("Checking png1A...\n"); + run_test_for(png1A); + dump("Checking png1B...\n"); + run_test_for(png1B); + dump("Checking png2A...\n"); + run_test_for(png2A); + dump("Checking png2B...\n"); + run_test_for(png2B); +} + +function run_test_for(input) { + var encoder, dataURL; + + encoder = encodeImage(input); + dataURL = makeDataURL(encoder, "image/png"); + Assert.equal(dataURL, input.expected); + + encoder = encodeImageAsync(input); + dataURL = makeDataURLFromAsync(encoder, "image/png", input.expected); +} + +function encodeImage(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + + var frame = input.frames[0]; + encoder.initFromData( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + return encoder; +} + +function _encodeImageAsyncFactory(frame, options, encoder) { + function finishEncode() { + encoder.addImageFrame( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + encoder.endImageEncode(); + } + return finishEncode; +} + +function encodeImageAsync(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + + var frame = input.frames[0]; + encoder.startImageEncode(frame.width, frame.height, frame.format, options); + + do_timeout(50, _encodeImageAsyncFactory(frame, options, encoder)); + return encoder; +} + +function makeDataURL(encoder, mimetype) { + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); + + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + stream.QueryInterface(Ci.nsIBinaryInputStream); + + stream.setInputStream(rawStream); + + var bytes = stream.readByteArray(stream.available()); // returns int[] + + var base64String = toBase64(bytes); + + return "data:" + mimetype + ";base64," + base64String; +} + +function makeDataURLFromAsync(encoder, mimetype, expected) { + do_test_pending(); + var rawStream = encoder.QueryInterface(Ci.nsIAsyncInputStream); + + var currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + var bytes = []; + + var binarystream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + binarystream.QueryInterface(Ci.nsIBinaryInputStream); + + var asyncReader = { + onInputStreamReady(stream) { + binarystream.setInputStream(stream); + var available = 0; + try { + available = stream.available(); + } catch (e) {} + + if (available > 0) { + bytes = bytes.concat(binarystream.readByteArray(available)); + stream.asyncWait(this, 0, 0, currentThread); + } else { + var base64String = toBase64(bytes); + var dataURL = "data:" + mimetype + ";base64," + base64String; + Assert.equal(dataURL, expected); + do_test_finished(); + } + }, + }; + rawStream.asyncWait(asyncReader, 0, 0, currentThread); +} + +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = + // eslint-disable-next-line no-useless-concat + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +const base64Pad = "="; +function toBase64(data) { + var result = ""; + var length = data.length; + var i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < length - 2; i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length % 3) { + i = length - (length % 3); + result += toBase64Table[data[i] >> 2]; + if (length % 3 == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[(data[i + 1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +} diff --git a/image/test/unit/test_imgtools.js b/image/test/unit/test_imgtools.js new file mode 100644 index 0000000000..7f832a6b12 --- /dev/null +++ b/image/test/unit/test_imgtools.js @@ -0,0 +1,869 @@ +/* + * Tests for imgITools + */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/* + * dumpToFile() + * + * For test development, dumps the specified array to a file. + * Call |dumpToFile(outData);| in a test to file to a file. + */ +// eslint-disable-next-line no-unused-vars +function dumpToFile(aData) { + var outputFile = do_get_cwd(); + outputFile.append("testdump.webp"); + + var outputStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + // WR_ONLY|CREATE|TRUNC + outputStream.init(outputFile, 0x02 | 0x08 | 0x20, 0o644, null); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(outputStream); + + bos.writeByteArray(aData); + + outputStream.close(); +} + +/* + * getFileInputStream() + * + * Returns an input stream for the specified file. + */ +function getFileInputStream(aFile) { + var inputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + // init the stream as RD_ONLY, -1 == default permissions. + inputStream.init(aFile, 0x01, -1, null); + + // Blah. The image decoders use ReadSegments, which isn't implemented on + // file input streams. Use a buffered stream to make it work. + var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance( + Ci.nsIBufferedInputStream + ); + bis.init(inputStream, 1024); + + return bis; +} + +/* + * streamToArray() + * + * Consumes an input stream, and returns its bytes as an array. + */ +function streamToArray(aStream) { + var size = aStream.available(); + + // use a binary input stream to grab the bytes. + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(aStream); + + var bytes = bis.readByteArray(size); + if (size != bytes.length) { + throw new Error("Didn't read expected number of bytes"); + } + + return bytes; +} + +/* + * compareArrays + * + * Compares two arrays, and throws if there's a difference. + */ +function compareArrays(aArray1, aArray2) { + Assert.equal(aArray1.length, aArray2.length); + + for (var i = 0; i < aArray1.length; i++) { + if (aArray1[i] != aArray2[i]) { + throw new Error("arrays differ at index " + i); + } + } +} + +/* + * checkExpectedError + * + * Checks to see if a thrown error was expected or not, and if it + * matches the expected value. + */ +function checkExpectedError(aExpectedError, aActualError) { + if (aExpectedError) { + if (!aActualError) { + throw new Error("Didn't throw as expected (" + aExpectedError + ")"); + } + + if (!aExpectedError.test(aActualError)) { + throw new Error("Threw (" + aActualError + "), not (" + aExpectedError); + } + + // We got the expected error, so make a note in the test log. + dump("...that error was expected.\n\n"); + } else if (aActualError) { + throw new Error("Threw unexpected error: " + aActualError); + } +} + +function run_test() { + try { + /* ========== 0 ========== */ + var testnum = 0; + var testdesc = "imgITools setup"; + var err = null; + + var imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); + + if (!imgTools) { + throw new Error("Couldn't get imgITools service"); + } + + // Ugh, this is an ugly hack. The pixel values we get in Windows are sometimes + // +/- 1 value compared to other platforms, so we need to compare against a + // different set of reference images. nsIXULRuntime.OS doesn't seem to be + // available in xpcshell, so we'll use this as a kludgy way to figure out if + // we're running on Windows. + var isWindows = mozinfo.os == "win"; + + /* ========== 1 ========== */ + testnum++; + testdesc = "test decoding a PNG"; + + // 64x64 png, 8415 bytes. + var imgName = "image1.png"; + var inMimeType = "image/png"; + var imgFile = do_get_file(imgName); + + var istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + var buffer = NetUtil.readInputStreamToString(istream, istream.available()); + var container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 64); + Assert.equal(container.height, 64); + + /* ========== 2 ========== */ + testnum++; + testdesc = "test encoding a scaled JPEG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 16, 16); + + var encodedBytes = streamToArray(istream); + // Get bytes for expected result + var refName = "image1png16x16.jpg"; + var refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1050); + var referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 3 ========== */ + testnum++; + testdesc = "test encoding an unscaled JPEG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/jpeg"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1png64x64.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 4507); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 4 ========== */ + testnum++; + testdesc = "test decoding a JPEG"; + + // 32x32 jpeg, 3494 bytes. + imgName = "image2.jpg"; + inMimeType = "image/jpeg"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 3494); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + /* ========== 5 ========== */ + testnum++; + testdesc = "test encoding a scaled PNG"; + + if (!isWindows) { + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/png", 16, 16); + + encodedBytes = streamToArray(istream); + // Get bytes for expected result + refName = isWindows ? "image2jpg16x16-win.png" : "image2jpg16x16.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 955); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + } + + /* ========== 6 ========== */ + testnum++; + testdesc = "test encoding an unscaled PNG"; + + if (!isWindows) { + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/png"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = isWindows ? "image2jpg32x32-win.png" : "image2jpg32x32.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3026); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + } + + /* ========== 7 ========== */ + testnum++; + testdesc = "test decoding a ICO"; + + // 16x16 ico, 1406 bytes. + imgName = "image3.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 1406); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 16); + Assert.equal(container.height, 16); + + /* ========== 8 ========== */ + testnum++; + testdesc = "test encoding a scaled PNG"; // note that we're scaling UP + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/png", 32, 32); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image3ico32x32.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 2280); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 9 ========== */ + testnum++; + testdesc = "test encoding an unscaled PNG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/png"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image3ico16x16.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 520); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 10 ========== */ + testnum++; + testdesc = "test decoding a GIF"; + + // 32x32 gif, 1809 bytes. + imgName = "image4.gif"; + inMimeType = "image/gif"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 1809); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + /* ========== 11 ========== */ + testnum++; + testdesc = + "test encoding an unscaled ICO with format options " + + "(format=bmp;bpp=32)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage( + container, + "image/vnd.microsoft.icon", + "format=bmp;bpp=32" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif32x32bmp32bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 4286); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 12 ========== */ + testnum++; + testdesc = + // eslint-disable-next-line no-useless-concat + "test encoding a scaled ICO with format options " + "(format=bmp;bpp=32)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage( + container, + "image/vnd.microsoft.icon", + 16, + 16, + "format=bmp;bpp=32" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif16x16bmp32bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1150); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 13 ========== */ + testnum++; + testdesc = + "test encoding an unscaled ICO with format options " + + "(format=bmp;bpp=24)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage( + container, + "image/vnd.microsoft.icon", + "format=bmp;bpp=24" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif32x32bmp24bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3262); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 14 ========== */ + testnum++; + testdesc = + // eslint-disable-next-line no-useless-concat + "test encoding a scaled ICO with format options " + "(format=bmp;bpp=24)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage( + container, + "image/vnd.microsoft.icon", + 16, + 16, + "format=bmp;bpp=24" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif16x16bmp24bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 894); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 15 ========== */ + testnum++; + testdesc = "test cropping a JPG"; + + // 32x32 jpeg, 3494 bytes. + imgName = "image2.jpg"; + inMimeType = "image/jpeg"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 3494); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + // encode a cropped image + istream = imgTools.encodeCroppedImage( + container, + "image/jpeg", + 0, + 0, + 16, + 16 + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x16cropped.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 879); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 16 ========== */ + testnum++; + testdesc = "test cropping a JPG with an offset"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage( + container, + "image/jpeg", + 16, + 16, + 16, + 16 + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x16cropped2.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 878); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 17 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 16, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x32cropped3.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1127); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 18 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given width"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 0, 16); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x16cropped4.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1135); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 19 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given width and height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 0, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x32.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1634); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 20 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given width"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 0, 16); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x16scaled.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1227); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 21 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 16, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x32scaled.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1219); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 22 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given width and height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 0, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x32.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1634); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 23 ========== */ + testnum++; + testdesc = "test invalid arguments for cropping"; + + var numErrors = 0; + + try { + // width/height can't be negative + imgTools.encodeScaledImage(container, "image/jpeg", -1, -1); + } catch (e) { + numErrors++; + } + + try { + // offsets can't be negative + imgTools.encodeCroppedImage(container, "image/jpeg", -1, -1, 16, 16); + } catch (e) { + numErrors++; + } + + try { + // width/height can't be negative + imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, -1, -1); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 17, 17, 16, 16); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 33, 33); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 1, 1, 0, 0); + } catch (e) { + numErrors++; + } + + Assert.equal(numErrors, 6); + + /* ========== 24 ========== */ + testnum++; + testdesc = "test encoding webp"; + + // Load picture that we want to convert + imgName = "image1.png"; + inMimeType = "image/png"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // Convert image to webp + istream = imgTools.encodeImage(container, "image/webp"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1.webp"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3206); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 25 ========== */ + testnum++; + testdesc = "test encoding webp with quality parameter"; + + // Load picture that we want to convert + imgName = "image1.png"; + inMimeType = "image/png"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // Convert image to webp + istream = imgTools.encodeImage(container, "image/webp", "quality=50"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1quality50.webp"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1944); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== bug 363986 ========== */ + testnum = 363986; + testdesc = "test PNG and JPEG and WEBP encoders' Read/ReadSegments methods"; + + var testData = [ + { + preImage: "image3.ico", + preImageMimeType: "image/x-icon", + refImage: "image3ico16x16.png", + refImageMimeType: "image/png", + }, + { + preImage: "image1.png", + preImageMimeType: "image/png", + refImage: "image1png64x64.jpg", + refImageMimeType: "image/jpeg", + }, + { + preImage: "image1.png", + preImageMimeType: "image/png", + refImage: "image1.webp", + refImageMimeType: "image/webp", + }, + ]; + + for (var i = 0; i < testData.length; ++i) { + var dict = testData[i]; + + imgFile = do_get_file(dict.refImage); + istream = getFileInputStream(imgFile); + var refBytes = streamToArray(istream); + + imgFile = do_get_file(dict.preImage); + istream = getFileInputStream(imgFile); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + dict.preImageMimeType + ); + + istream = imgTools.encodeImage(container, dict.refImageMimeType); + + var sstream = Cc["@mozilla.org/storagestream;1"].createInstance( + Ci.nsIStorageStream + ); + sstream.init(4096, 4294967295, null); + var ostream = sstream.getOutputStream(0); + var bostream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + + // use a tiny buffer to make sure the image data doesn't fully fit in it + bostream.init(ostream, 8); + + bostream.writeFrom(istream, istream.available()); + bostream.flush(); + bostream.close(); + + var encBytes = streamToArray(sstream.newInputStream(0)); + + compareArrays(refBytes, encBytes); + } + + /* ========== bug 413512 ========== */ + testnum = 413512; + testdesc = "test decoding bad favicon (bug 413512)"; + + imgName = "bug413512.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 17759); + var errsrc = "none"; + + try { + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // We expect to hit an error during encoding because the ICO header of the + // image is fine, but the actual resources are corrupt. Since + // decodeImageFromBuffer() only performs a metadata decode, it doesn't decode + // far enough to realize this, but we'll find out when we do a full decode + // during encodeImage(). + try { + istream = imgTools.encodeImage(container, "image/png"); + } catch (e) { + err = e; + errsrc = "encode"; + } + } catch (e) { + err = e; + errsrc = "decode"; + } + + Assert.equal(errsrc, "encode"); + checkExpectedError(/NS_ERROR_FAILURE/, err); + + /* ========== bug 815359 ========== */ + testnum = 815359; + testdesc = "test correct ico hotspots (bug 815359)"; + + imgName = "bug815359.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 4286); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + Assert.equal(container.hotspotX, 10); + Assert.equal(container.hotspotY, 9); + + /* ========== end ========== */ + } catch (e) { + throw new Error( + "FAILED in test #" + testnum + " -- " + testdesc + ": " + e + ); + } +} diff --git a/image/test/unit/test_moz_icon_uri.js b/image/test/unit/test_moz_icon_uri.js new file mode 100644 index 0000000000..0111d71d2a --- /dev/null +++ b/image/test/unit/test_moz_icon_uri.js @@ -0,0 +1,157 @@ +/* + * Test icon URI functionality + * + */ + +// There are 3 types of valid icon URIs: +// 1. moz-icon:[valid URL] +// 2. moz-icon://[file name] +// 3. moz-icon://stock/[icon identifier] +// Plus we also support moz-icon://[valid URL] for backwards compatibility. + +// Main test entry point. + +function run_test() { + let ioService = Services.io; + let currentSpec = ""; // the uri spec that we're currently testing + let exception = false; // whether or not an exception was thrown + let uri = null; // the current URI + let iconURI = null; // the current icon URI + + // Note that if the scheme is not correct the ioservice won't even create an icon URI + // so don't bother testing incorrect schemes here. + + // Make sure a valid file name icon URI can be created and that we can obtain + // all arguments, the spec, and the file extension. + currentSpec = "moz-icon://foo.html?contentType=bar&size=button&state=normal"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.iconSize, "button"); + Assert.equal(iconURI.iconState, "normal"); + Assert.equal(iconURI.contentType, "bar"); + Assert.equal(iconURI.fileExtension, ".html"); + + // Make sure a valid file name icon URI can be created with a numeric size, + // and make sure the numeric size is handled properly + currentSpec = "moz-icon://foo.html?size=3"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.iconSize, ""); + Assert.equal(iconURI.imageSize, 3); + + // Make sure a valid stock icon URI can be created and that we can obtain + // the stock icon's name. + currentSpec = "moz-icon://stock/foo"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.stockIcon, "foo"); + + // Make sure an invalid stock icon URI, missing icon identifier, throws. + currentSpec = "moz-icon://stock/?size=3"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.ok(exception); + exception = false; // reset exception value + + // Make sure a valid file URL icon URI can be created and that we can obtain + // the URL and QI it to an nsIFileURL. + currentSpec = "moz-icon:file://foo.txt"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + let fileURL = null; + try { + fileURL = iconURI.iconURL.QueryInterface(Ci.nsIFileURL); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + Assert.notEqual(fileURL, null); + + // Now test a file URI which has been created with an extra // + currentSpec = "moz-icon://file://foo.txt"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + fileURL = null; + try { + fileURL = iconURI.iconURL.QueryInterface(Ci.nsIFileURL); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + Assert.notEqual(fileURL, null); + + // Now test a simple invalid icon URI. This should fail. + currentSpec = "moz-icon:foo"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value + + // Now test an icon URI that has a URI for a path but that is not a URL. This should fail. + // This is png data for a little red dot that I got from wikipedia. + currentSpec = + "moz-icon:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg=="; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value + + // Now test a URI that should be a file name but is ridiculously long. This should fail. + currentSpec = + "moz-icon://data:application/vnd.ms-excel;base64,PHhtbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtOCI+PHNzOldvcmtib29rIHhtbG5zOnNzPSJ1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOm9mZmljZTpzcHJlYWRzaGVldCIgeG1sbnM6eD0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6ZXhjZWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+PG86RG9jdW1lbnRQcm9wZXJ0aWVzPjxvOlRpdGxlPkFycmF5IEdyaWQ8L286VGl0bGU+PC9vOkRvY3VtZW50UHJvcGVydGllcz48c3M6RXhjZWxXb3JrYm9vaz48c3M6V2luZG93SGVpZ2h0PjkwMDA8L3NzOldpbmRvd0hlaWdodD48c3M6V2luZG93V2lkdGg+MTc0ODA8L3NzOldpbmRvd1dpZHRoPjxzczpQcm90ZWN0U3RydWN0dXJlPkZhbHNlPC9zczpQcm90ZWN0U3RydWN0dXJlPjxzczpQcm90ZWN0V2luZG93cz5GYWxzZTwvc3M6UHJvdGVjdFdpbmRvd3M+PC9zczpFeGNlbFdvcmtib29rPjxzczpTdHlsZXM+PHNzOlN0eWxlIHNzOklEPSJEZWZhdWx0Ij48c3M6QWxpZ25tZW50IHNzOlZlcnRpY2FsPSJUb3AiIHNzOldyYXBUZXh0PSIxIiAvPjxzczpGb250IHNzOkZvbnROYW1lPSJhcmlhbCIgc3M6U2l6ZT0iMTAiIC8+PHNzOkJvcmRlcnM+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJUb3AiIC8+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJCb3R0b20iIC8+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJMZWZ0IiAvPjxzczpCb3JkZXIgc3M6Q29sb3I9IiNlNGU0ZTQiIHNzOldlaWdodD0iMSIgc3M6TGluZVN0eWxlPSJDb250aW51b3VzIiBzczpQb3NpdGlvbj0iUmlnaHQiIC8+PC9zczpCb3JkZXJzPjxzczpJbnRlcmlvciAvPjxzczpOdW1iZXJGb3JtYXQgLz48c3M6UHJvdGVjdGlvbiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOklEPSJ0aXRsZSI+PHNzOkJvcmRlcnMgLz48c3M6Rm9udCAvPjxzczpBbGlnbm1lbnQgc3M6V3JhcFRleHQ9IjEiIHNzOlZlcnRpY2FsPSJDZW50ZXIiIHNzOkhvcml6b250YWw9IkNlbnRlciIgLz48c3M6TnVtYmVyRm9ybWF0IHNzOkZvcm1hdD0iQCIgLz48L3NzOlN0eWxlPjxzczpTdHlsZSBzczpJRD0iaGVhZGVyY2VsbCI+PHNzOkZvbnQgc3M6Qm9sZD0iMSIgc3M6U2l6ZT0iMTAiIC8+PHNzOkFsaWdubWVudCBzczpXcmFwVGV4dD0iMSIgc3M6SG9yaXpvbnRhbD0iQ2VudGVyIiAvPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNBM0M5RjEiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6SUQ9ImV2ZW4iPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNDQ0ZGRkYiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJldmVuIiBzczpJRD0iZXZlbmRhdGUiPjxzczpOdW1iZXJGb3JtYXQgc3M6Rm9ybWF0PSJ5eXl5LW1tLWRkIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOlBhcmVudD0iZXZlbiIgc3M6SUQ9ImV2ZW5pbnQiPjxzczpOdW1iZXJGb3JtYXQgc3M6Rm9ybWF0PSIwIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOlBhcmVudD0iZXZlbiIgc3M6SUQ9ImV2ZW5mbG9hdCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAuMDAwIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOklEPSJvZGQiPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNDQ0NDRkYiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJvZGQiIHNzOklEPSJvZGRkYXRlIj48c3M6TnVtYmVyRm9ybWF0IHNzOkZvcm1hdD0ieXl5eS1tbS1kZCIgLz48L3NzOlN0eWxlPjxzczpTdHlsZSBzczpQYXJlbnQ9Im9kZCIgc3M6SUQ9Im9kZGludCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJvZGQiIHNzOklEPSJvZGRmbG9hdCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAuMDAwIiAvPjwvc3M6U3R5bGU+PC9zczpTdHlsZXM+PHNzOldvcmtzaGVldCBzczpOYW1lPSJBcnJheSBHcmlkIj48c3M6TmFtZXM+PHNzOk5hbWVkUmFuZ2Ugc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiBzczpSZWZlcnNUbz0iPSdBcnJheSBHcmlkJyFSMTpSMiIgLz48L3NzOk5hbWVzPjxzczpUYWJsZSB4OkZ1bGxSb3dzPSIxIiB4OkZ1bGxDb2x1bW5zPSIxIiBzczpFeHBhbmRlZENvbHVtbkNvdW50PSI1IiBzczpFeHBhbmRlZFJvd0NvdW50PSIzMSI+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSIyNzEiIC8+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSI3NSIgLz48c3M6Q29sdW1uIHNzOkF1dG9GaXRXaWR0aD0iMSIgc3M6V2lkdGg9Ijc1IiAvPjxzczpDb2x1bW4gc3M6QXV0b0ZpdFdpZHRoPSIxIiBzczpXaWR0aD0iNzUiIC8+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSI4NSIgLz48c3M6Um93IHNzOkhlaWdodD0iMzgiPjxzczpDZWxsIHNzOlN0eWxlSUQ9InRpdGxlIiBzczpNZXJnZUFjcm9zcz0iNCI+PHNzOkRhdGEgeG1sbnM6aHRtbD0iaHR0cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MCIgc3M6VHlwZT0iU3RyaW5nIj48aHRtbDpCPiAoYykyMDA4IFNFQk4gVUE8L2h0bWw6Qj48L3NzOkRhdGE+PHNzOk5hbWVkQ2VsbCBzczpOYW1lPSJQcmludF9UaXRsZXMiIC8+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3cgc3M6QXV0b0ZpdEhlaWdodD0iMSI+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Db21wYW55PC9zczpEYXRhPjxzczpOYW1lZENlbGwgc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiAvPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJoZWFkZXJjZWxsIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlByaWNlPC9zczpEYXRhPjxzczpOYW1lZENlbGwgc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiAvPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJoZWFkZXJjZWxsIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkNoYW5nZTwvc3M6RGF0YT48c3M6TmFtZWRDZWxsIHNzOk5hbWU9IlByaW50X1RpdGxlcyIgLz48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4lIENoYW5nZTwvc3M6RGF0YT48c3M6TmFtZWRDZWxsIHNzOk5hbWU9IlByaW50X1RpdGxlcyIgLz48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5MYXN0IFVwZGF0ZWQ8L3NzOkRhdGE+PHNzOk5hbWVkQ2VsbCBzczpOYW1lPSJQcmludF9UaXRsZXMiIC8+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zbSBDbzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj43MS43Mjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BVCZUIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zMS42MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPi0wLjQ4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTEuNTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BbGNvYSBJbmM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MjkuMDE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC40Mjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjQ3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+QWx0cmlhIEdyb3VwIEluYzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjgzLjgxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4yODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMzQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BbWVyaWNhbiBFeHByZXNzIENvbXBhbnk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NTIuNTU8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+QW1lcmljYW4gSW50ZXJuYXRpb25hbCBHcm91cCwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjY0LjEzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4zMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNDk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Cb2VpbmcgQ28uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjc1LjQzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC43MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkNhdGVycGlsbGFyIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj42Ny4yNzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuOTI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjM5PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+Q2l0aWdyb3VwLCBJbmMuPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjQ5LjM3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wNDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkUuSS4gZHUgUG9udCBkZSBOZW1vdXJzIGFuZCBDb21wYW55PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDAuNDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjUxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MS4yODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkV4eG9uIE1vYmlsIENvcnA8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjguMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC40Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC42NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkdlbmVyYWwgRWxlY3RyaWMgQ29tcGFueTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjM0LjE0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTAuMDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC4yMzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkdlbmVyYWwgTW90b3JzIENvcnBvcmF0aW9uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjMwLjI3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+My43NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkhld2xldHQtUGFja2FyZCBDby48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNi41Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPi0wLjAzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTAuMDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Ib25leXdlbGwgSW50bCBJbmM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MzguNzc8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wNTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjEzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+SW50ZWwgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xOS44ODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMzE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjU4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+SW50ZXJuYXRpb25hbCBCdXNpbmVzcyBNYWNoaW5lczwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj44MS40MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQ0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5KUCBNb3JnYW4gJiBDaGFzZSAmIENvPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDUuNzM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjA3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4xNTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkpvaG5zb24gJiBKb2huc29uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjY0LjcyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wOTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPk1jRG9uYWxkJ3MgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNi43Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuODY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yLjQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5NZXJjayAmIENvLiwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj40MC45Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5NaWNyb3NvZnQgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yNS44NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjU0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+UGZpemVyIEluYzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yNy45Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MS40NTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlRoZSBDb2NhLUNvbGEgQ29tcGFueTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjQ1LjA3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4yNjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5UaGUgSG9tZSBEZXBvdCwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNC42NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjM1PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5UaGUgUHJvY3RlciAmIEdhbWJsZSBDb21wYW55PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjEuOTE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wMjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlVuaXRlZCBUZWNobm9sb2dpZXMgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjMuMjY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC41NTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjg4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+VmVyaXpvbiBDb21tdW5pY2F0aW9uczwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjM1LjU3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4zOTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMTE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XYWwtTWFydCBTdG9yZXMsIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDUuNDU8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC43Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjYzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PC9zczpUYWJsZT48eDpXb3Jrc2hlZXRPcHRpb25zPjx4OlBhZ2VTZXR1cD48eDpMYXlvdXQgeDpDZW50ZXJIb3Jpem9udGFsPSIxIiB4Ok9yaWVudGF0aW9uPSJMYW5kc2NhcGUiIC8+PHg6Rm9vdGVyIHg6RGF0YT0iUGFnZSAmYW1wO1Agb2YgJmFtcDtOIiB4Ok1hcmdpbj0iMC41IiAvPjx4OlBhZ2VNYXJnaW5zIHg6VG9wPSIwLjUiIHg6UmlnaHQ9IjAuNSIgeDpMZWZ0PSIwLjUiIHg6Qm90dG9tPSIwLjgiIC8+PC94OlBhZ2VTZXR1cD48eDpGaXRUb1BhZ2UgLz48eDpQcmludD48eDpQcmludEVycm9ycz5CbGFuazwveDpQcmludEVycm9ycz48eDpGaXRXaWR0aD4xPC94OkZpdFdpZHRoPjx4OkZpdEhlaWdodD4zMjc2NzwveDpGaXRIZWlnaHQ+PHg6VmFsaWRQcmludGVySW5mbyAvPjx4OlZlcnRpY2FsUmVzb2x1dGlvbj42MDA8L3g6VmVydGljYWxSZXNvbHV0aW9uPjwveDpQcmludD48eDpTZWxlY3RlZCAvPjx4OkRvTm90RGlzcGxheUdyaWRsaW5lcyAvPjx4OlByb3RlY3RPYmplY3RzPkZhbHNlPC94OlByb3RlY3RPYmplY3RzPjx4OlByb3RlY3RTY2VuYXJpb3M+RmFsc2U8L3g6UHJvdGVjdFNjZW5hcmlvcz48L3g6V29ya3NoZWV0T3B0aW9ucz48L3NzOldvcmtzaGVldD48L3NzOldvcmtib29rPg=="; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value +} diff --git a/image/test/unit/test_private_channel.js b/image/test/unit/test_private_channel.js new file mode 100644 index 0000000000..008f6aad92 --- /dev/null +++ b/image/test/unit/test_private_channel.js @@ -0,0 +1,166 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +var server = new HttpServer(); +server.registerPathHandler("/image.png", imageHandler); +server.start(-1); + +/* import-globals-from image_load_helpers.js */ +load("image_load_helpers.js"); + +var gHits = 0; + +var gIoService = Services.io; +var gPublicLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader +); +var gPrivateLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader +); +gPrivateLoader.QueryInterface(Ci.imgICache).respectPrivacyNotifications(); + +var nonPrivateLoadContext = Cu.createLoadContext(); +var privateLoadContext = Cu.createPrivateLoadContext(); + +function imageHandler(metadata, response) { + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=" + ); + response.bodyOutputStream.write(body, body.length); +} + +var requests = []; +var listeners = []; + +var gImgPath = "http://localhost:" + server.identity.primaryPort + "/image.png"; + +function setup_chan(path, isPrivate, callback) { + var uri = NetUtil.newURI(gImgPath); + var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + var principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: isPrivate ? 1 : 0, + }); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE, + }); + chan.notificationCallbacks = isPrivate + ? privateLoadContext + : nonPrivateLoadContext; + var channelListener = new ChannelListener(); + chan.asyncOpen(channelListener); + + var listener = new ImageListener(null, callback); + var outlistener = {}; + var loader = isPrivate ? gPrivateLoader : gPublicLoader; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + listeners.push(outer); + requests.push( + loader.loadImageWithChannelXPCOM(chan, outer, null, outlistener) + ); + channelListener.outputListener = outlistener.value; + listener.synchronous = false; +} + +function loadImage(isPrivate, callback) { + var listener = new ImageListener(null, callback); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var uri = gIoService.newURI(gImgPath); + var loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + loadGroup.notificationCallbacks = isPrivate + ? privateLoadContext + : nonPrivateLoadContext; + var loader = isPrivate ? gPrivateLoader : gPublicLoader; + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push( + loader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + loadGroup, + outer, + null, + 0, + null + ) + ); + listener.synchronous = false; +} + +function run_loadImage_tests() { + function observer() { + Services.obs.removeObserver(observer, "cacheservice:empty-cache"); + gHits = 0; + loadImage(false, function () { + loadImage(false, function () { + loadImage(true, function () { + loadImage(true, function () { + Assert.equal(gHits, 2); + server.stop(do_test_finished); + }); + }); + }); + }); + } + + for (let loader of [gPublicLoader, gPrivateLoader]) { + loader.QueryInterface(Ci.imgICache).clearCache(true); + loader.QueryInterface(Ci.imgICache).clearCache(false); + } + Services.obs.addObserver(observer, "cacheservice:empty-cache"); + let cs = Services.cache2; + cs.clear(); +} + +function cleanup() { + for (var i = 0; i < requests.length; ++i) { + requests[i].cancelAndForgetObserver(0); + } +} + +function run_test() { + registerCleanupFunction(cleanup); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + // We create a public channel that loads an image, then an identical + // one that should cause a cache read. We then create a private channel + // and load the same image, and do that a second time to ensure a cache + // read. In total, we should cause two separate http responses to occur, + // since the private channels shouldn't be able to use the public cache. + setup_chan("/image.png", false, function () { + setup_chan("/image.png", false, function () { + setup_chan("/image.png", true, function () { + setup_chan("/image.png", true, function () { + Assert.equal(gHits, 2); + run_loadImage_tests(); + }); + }); + }); + }); +} diff --git a/image/test/unit/xpcshell.ini b/image/test/unit/xpcshell.ini new file mode 100644 index 0000000000..7ce8a02e1e --- /dev/null +++ b/image/test/unit/xpcshell.ini @@ -0,0 +1,42 @@ +[DEFAULT] +head = +support-files = + async_load_tests.js + bug413512.ico + bug815359.ico + image1.png + image1.webp + image1quality50.webp + image1png16x16.jpg + image1png64x64.jpg + image2.jpg + image2jpg16x16-win.png + image2jpg16x16.png + image2jpg16x16cropped.jpg + image2jpg16x16cropped2.jpg + image2jpg16x32cropped3.jpg + image2jpg16x32scaled.jpg + image2jpg32x16cropped4.jpg + image2jpg32x16scaled.jpg + image2jpg32x32-win.png + image2jpg32x32.jpg + image2jpg32x32.png + image3.ico + image3ico16x16.png + image3ico32x32.png + image4.gif + image4gif16x16bmp24bpp.ico + image4gif16x16bmp32bpp.ico + image4gif32x32bmp24bpp.ico + image4gif32x32bmp32bpp.ico + image_load_helpers.js + + +[test_async_notification.js] +[test_async_notification_404.js] +[test_async_notification_animated.js] +[test_encoder_apng.js] +[test_encoder_png.js] +[test_imgtools.js] +[test_moz_icon_uri.js] +[test_private_channel.js] -- cgit v1.2.3