From 94f011895c8e1bde36ee2ec235dbbcf2c994ac4c Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 10 Feb 2017 17:18:18 -0800 Subject: [PATCH] [core] Make Image safer Provide Image::copy, which handles copying rectangles from a source to a destination, with thorough bounds checking. Also fixes an indexing error in SpriteAtlas, where the top row of pixels in a wrapped image was copied from the wrong source row. --- include/mbgl/util/image.hpp | 65 ++++++++++++++++-- include/mbgl/util/size.hpp | 4 ++ mapbox-gl-js | 2 +- src/mbgl/renderer/frame_history.cpp | 2 +- src/mbgl/renderer/frame_history.hpp | 2 +- src/mbgl/sprite/sprite_atlas.cpp | 56 +++++---------- src/mbgl/sprite/sprite_parser.cpp | 11 +-- src/mbgl/text/glyph.hpp | 3 +- src/mbgl/text/glyph_atlas.cpp | 36 ++-------- src/mbgl/text/glyph_atlas.hpp | 2 +- src/mbgl/text/glyph_pbf.cpp | 44 +++++++----- test/fixtures/sprite_atlas/basic/expected.png | Bin 1124 -> 694 bytes test/text/glyph_atlas.test.cpp | 15 ++-- test/text/glyph_pbf.test.cpp | 8 ++- test/util/image.test.cpp | 29 ++++++++ 15 files changed, 163 insertions(+), 116 deletions(-) diff --git a/include/mbgl/util/image.hpp b/include/mbgl/util/image.hpp index 2de764683..5d1462e7c 100644 --- a/include/mbgl/util/image.hpp +++ b/include/mbgl/util/image.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -24,6 +25,15 @@ public: : size(std::move(size_)), data(std::make_unique(bytes())) {} + Image(Size size_, const uint8_t* srcData, std::size_t srcLength) + : size(std::move(size_)) { + if (srcLength != bytes()) { + throw std::invalid_argument("mismatched image size"); + } + data = std::make_unique(bytes()); + std::copy(srcData, srcData + srcLength, data.get()); + } + Image(Size size_, std::unique_ptr data_) : size(std::move(size_)), data(std::move(data_)) {} @@ -38,10 +48,13 @@ public: return *this; } - bool operator==(const Image& rhs) const { - return size == rhs.size && - std::equal(data.get(), data.get() + bytes(), rhs.data.get(), - rhs.data.get() + rhs.bytes()); + friend bool operator==(const Image& lhs, const Image& rhs) { + return std::equal(lhs.data.get(), lhs.data.get() + lhs.bytes(), + rhs.data.get(), rhs.data.get() + rhs.bytes()); + } + + friend bool operator!=(const Image& lhs, const Image& rhs) { + return !(lhs == rhs); } bool valid() const { @@ -58,6 +71,50 @@ public: size_t stride() const { return channels * size.width; } size_t bytes() const { return stride() * size.height; } + void fill(uint8_t value) { + std::fill(data.get(), data.get() + bytes(), value); + } + + // Copy image data within `rect` from `src` to the rectangle of the same size at `pt` + // in `dst`. If the specified bounds exceed the bounds of the source or destination, + // throw `std::out_of_range`. Must not be used to move data within a single Image. + static void copy(const Image& srcImg, Image& dstImg, const Point& srcPt, const Point& dstPt, const Size& size) { + if (!srcImg.valid()) { + throw std::invalid_argument("invalid source for image copy"); + } + + if (!dstImg.valid()) { + throw std::invalid_argument("invalid destination for image copy"); + } + + if (size.width > srcImg.size.width || + size.height > srcImg.size.height || + srcPt.x > srcImg.size.width - size.width || + srcPt.y > srcImg.size.height - size.height) { + throw std::out_of_range("out of range source coordinates for image copy"); + } + + if (size.width > dstImg.size.width || + size.height > dstImg.size.height || + dstPt.x > dstImg.size.width - size.width || + dstPt.y > dstImg.size.height - size.height) { + throw std::out_of_range("out of range destination coordinates for image copy"); + } + + const uint8_t* srcData = srcImg.data.get(); + uint8_t* dstData = dstImg.data.get(); + + assert(srcData != dstData); + + for (uint32_t y = 0; y < size.height; y++) { + const std::size_t srcOffset = (srcPt.y + y) * srcImg.stride() + srcPt.x * channels; + const std::size_t dstOffset = (dstPt.y + y) * dstImg.stride() + dstPt.x * channels; + std::copy(srcData + srcOffset, + srcData + srcOffset + size.width * channels, + dstData + dstOffset); + } + } + Size size; static constexpr size_t channels = Mode == ImageAlphaMode::Exclusive ? 1 : 4; std::unique_ptr data; diff --git a/include/mbgl/util/size.hpp b/include/mbgl/util/size.hpp index 1af85bcff..79679a92f 100644 --- a/include/mbgl/util/size.hpp +++ b/include/mbgl/util/size.hpp @@ -13,6 +13,10 @@ public: constexpr Size(const uint32_t width_, const uint32_t height_) : width(width_), height(height_) { } + constexpr uint32_t area() const { + return width * height; + } + constexpr explicit operator bool() const { return width > 0 && height > 0; } diff --git a/mapbox-gl-js b/mapbox-gl-js index 69de96f53..dee3911a7 160000 --- a/mapbox-gl-js +++ b/mapbox-gl-js @@ -1 +1 @@ -Subproject commit 69de96f53ee41f0e2b8804491d236f0b1fe1a3cd +Subproject commit dee3911a7b5a1e8b2333d133a162af388673d1b9 diff --git a/src/mbgl/renderer/frame_history.cpp b/src/mbgl/renderer/frame_history.cpp index 1ee53d87b..a933a9004 100644 --- a/src/mbgl/renderer/frame_history.cpp +++ b/src/mbgl/renderer/frame_history.cpp @@ -8,7 +8,7 @@ namespace mbgl { FrameHistory::FrameHistory() { changeOpacities.fill(0); - std::fill(opacities.data.get(), opacities.data.get() + opacities.bytes(), 0); + opacities.fill(0); } void FrameHistory::record(const TimePoint& now, float zoom, const Duration& duration) { diff --git a/src/mbgl/renderer/frame_history.hpp b/src/mbgl/renderer/frame_history.hpp index fffbd113e..f2b11f5f4 100644 --- a/src/mbgl/renderer/frame_history.hpp +++ b/src/mbgl/renderer/frame_history.hpp @@ -26,7 +26,7 @@ public: private: std::array changeTimes; std::array changeOpacities; - const AlphaImage opacities{ { 256, 1 } }; + AlphaImage opacities{ { 256, 1 } }; int16_t previousZoomIndex = 0; TimePoint previousTime; diff --git a/src/mbgl/sprite/sprite_atlas.cpp b/src/mbgl/sprite/sprite_atlas.cpp index ea055ce5e..61c074a94 100644 --- a/src/mbgl/sprite/sprite_atlas.cpp +++ b/src/mbgl/sprite/sprite_atlas.cpp @@ -231,52 +231,32 @@ optional SpriteAtlas::getPosition(const std::string& name, }; } -void copyBitmap(const uint32_t *src, const uint32_t srcStride, const uint32_t srcX, const uint32_t srcY, - uint32_t *const dst, const uint32_t dstStride, const uint32_t dstX, const uint32_t dstY, int dstSize, - const int width, const int height, const SpritePatternMode mode) { - - int srcI = srcY * srcStride + srcX; - int dstI = dstY * dstStride + dstX; - int x, y; - - if (mode == SpritePatternMode::Repeating) { - // add 1 pixel wrapped padding on each side of the image - dstI -= dstStride; - for (y = -1; y <= height; y++, srcI = ((y + height) % height + srcY) * srcStride + srcX, dstI += dstStride) { - for (x = -1; x <= width; x++) { - const int dstIndex = (dstI + x + dstSize) % dstSize; - dst[dstIndex] = src[srcI + ((x + width) % width)]; - } - } - - } else { - for (y = 0; y < height; y++, srcI += srcStride, dstI += dstStride) { - for (x = 0; x < width; x++) { - const int dstIndex = (dstI + x + dstSize) % dstSize; - dst[dstIndex] = src[srcI + x]; - } - } - } -} - void SpriteAtlas::copy(const Holder& holder, const SpritePatternMode mode) { if (!image.valid()) { image = PremultipliedImage({ static_cast(std::ceil(size.width * pixelRatio)), static_cast(std::ceil(size.height * pixelRatio)) }); - std::fill(image.data.get(), image.data.get() + image.bytes(), 0); + image.fill(0); } - const uint32_t* srcData = - reinterpret_cast(holder.spriteImage->image.data.get()); - if (!srcData) return; - uint32_t* const dstData = reinterpret_cast(image.data.get()); + if (!holder.spriteImage->image.valid()) { + return; + } - const int padding = 1; + const uint32_t padding = 1; + const uint32_t x = (holder.pos.x + padding) * pixelRatio; + const uint32_t y = (holder.pos.y + padding) * pixelRatio; + const uint32_t w = holder.spriteImage->image.size.width; + const uint32_t h = holder.spriteImage->image.size.height; - copyBitmap(srcData, holder.spriteImage->image.size.width, 0, 0, dstData, image.size.width, - (holder.pos.x + padding) * pixelRatio, (holder.pos.y + padding) * pixelRatio, - image.size.width * image.size.height, holder.spriteImage->image.size.width, - holder.spriteImage->image.size.height, mode); + PremultipliedImage::copy(holder.spriteImage->image, image, { 0, 0 }, { x, y }, { w, h }); + + if (mode == SpritePatternMode::Repeating) { + // Add 1 pixel wrapped padding on each side of the image. + PremultipliedImage::copy(holder.spriteImage->image, image, { 0, h - 1 }, { x, y - 1 }, { w, 1 }); // T + PremultipliedImage::copy(holder.spriteImage->image, image, { 0, 0 }, { x, y + h }, { w, 1 }); // B + PremultipliedImage::copy(holder.spriteImage->image, image, { w - 1, 0 }, { x - 1, y }, { 1, h }); // L + PremultipliedImage::copy(holder.spriteImage->image, image, { 0, 0 }, { x + w, y }, { 1, h }); // R + } dirty = true; } diff --git a/src/mbgl/sprite/sprite_parser.cpp b/src/mbgl/sprite/sprite_parser.cpp index 9de8515e1..66b5ec060 100644 --- a/src/mbgl/sprite/sprite_parser.cpp +++ b/src/mbgl/sprite/sprite_parser.cpp @@ -34,17 +34,8 @@ SpriteImagePtr createSpriteImage(const PremultipliedImage& image, PremultipliedImage dstImage({ width, height }); - auto srcData = reinterpret_cast(image.data.get()); - auto dstData = reinterpret_cast(dstImage.data.get()); - // Copy from the source image into our individual sprite image - for (uint32_t y = 0; y < height; ++y) { - const auto dstRow = y * width; - const auto srcRow = (y + srcY) * image.size.width + srcX; - for (uint32_t x = 0; x < width; ++x) { - dstData[dstRow + x] = srcData[srcRow + x]; - } - } + PremultipliedImage::copy(image, dstImage, { srcX, srcY }, { 0, 0 }, { width, height }); return std::make_unique(std::move(dstImage), ratio, sdf); } diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index c89d045df..78ea613cd 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -89,7 +90,7 @@ public: uint32_t id = 0; // A signed distance field of the glyph with a border (see above). - std::string bitmap; + AlphaImage bitmap; // Glyph metrics GlyphMetrics metrics; diff --git a/src/mbgl/text/glyph_atlas.cpp b/src/mbgl/text/glyph_atlas.cpp index dc1fe2b1d..88b319366 100644 --- a/src/mbgl/text/glyph_atlas.cpp +++ b/src/mbgl/text/glyph_atlas.cpp @@ -124,48 +124,26 @@ Rect GlyphAtlas::addGlyph(uintptr_t tileUID, return Rect{ 0, 0, 0, 0 }; } - uint16_t buffered_width = glyph.metrics.width + SDFGlyph::borderSize * 2; - uint16_t buffered_height = glyph.metrics.height + SDFGlyph::borderSize * 2; - - // Guard against mismatches between the glyph bitmap size and the size mandated by - // its metrics. - if (size_t(buffered_width * buffered_height) != glyph.bitmap.size()) { - return Rect{ 0, 0, 0, 0 }; - } - // Add a 1px border around every image. - const uint16_t padding = 1; - uint16_t pack_width = buffered_width + 2 * padding; - uint16_t pack_height = buffered_height + 2 * padding; + const uint32_t padding = 1; + uint16_t width = glyph.bitmap.size.width + 2 * padding; + uint16_t height = glyph.bitmap.size.height + 2 * padding; // Increase to next number divisible by 4, but at least 1. // This is so we can scale down the texture coordinates and pack them // into 2 bytes rather than 4 bytes. - pack_width += (4 - pack_width % 4); - pack_height += (4 - pack_height % 4); + width += (4 - width % 4); + height += (4 - height % 4); - Rect rect = bin.allocate(pack_width, pack_height); + Rect rect = bin.allocate(width, height); if (rect.w == 0) { Log::Error(Event::OpenGL, "glyph bitmap overflow"); return rect; } - // Verify that binpack didn't produce a rect that goes beyond the size of the image. - // This should never happen. - assert(rect.x + rect.w <= image.size.width); - assert(rect.y + rect.h <= image.size.height); - face.emplace(glyph.id, GlyphValue { rect, tileUID }); - // Copy the bitmap - const uint8_t* source = reinterpret_cast(glyph.bitmap.data()); - for (uint32_t y = 0; y < buffered_height; y++) { - uint32_t y1 = image.size.width * (rect.y + y + padding) + rect.x + padding; - uint32_t y2 = buffered_width * y; - for (uint32_t x = 0; x < buffered_width; x++) { - image.data[y1 + x] = source[y2 + x]; - } - } + AlphaImage::copy(glyph.bitmap, image, { 0, 0 }, { rect.x + padding, rect.y + padding }, glyph.bitmap.size); dirty = true; diff --git a/src/mbgl/text/glyph_atlas.hpp b/src/mbgl/text/glyph_atlas.hpp index af14aace5..84f29aeb7 100644 --- a/src/mbgl/text/glyph_atlas.hpp +++ b/src/mbgl/text/glyph_atlas.hpp @@ -101,7 +101,7 @@ private: std::mutex mtx; BinPack bin; std::unordered_map, FontStackHash> index; - const AlphaImage image; + AlphaImage image; std::atomic dirty; mbgl::optional texture; }; diff --git a/src/mbgl/text/glyph_pbf.cpp b/src/mbgl/text/glyph_pbf.cpp index cdeac5798..5c57d278d 100644 --- a/src/mbgl/text/glyph_pbf.cpp +++ b/src/mbgl/text/glyph_pbf.cpp @@ -25,6 +25,7 @@ void parseGlyphPBF(GlyphSet& glyphSet, const GlyphRange& glyphRange, const std:: auto glyph_pbf = fontstack_pbf.get_message(); SDFGlyph glyph; + protozero::data_view glyphData; bool hasID = false, hasWidth = false, hasHeight = false, hasLeft = false, hasTop = false, hasAdvance = false; @@ -36,7 +37,7 @@ void parseGlyphPBF(GlyphSet& glyphSet, const GlyphRange& glyphRange, const std:: hasID = true; break; case 2: // bitmap - glyph.bitmap = glyph_pbf.get_string(); + glyphData = glyph_pbf.get_view(); break; case 3: // width glyph.metrics.width = glyph_pbf.get_uint32(); @@ -64,26 +65,35 @@ void parseGlyphPBF(GlyphSet& glyphSet, const GlyphRange& glyphRange, const std:: } } + // Only treat this glyph as a correct glyph if it has all required fields. It also + // needs to satisfy a few metrics conditions that ensure that the glyph isn't bogus. + // All other glyphs are malformed. We're also discarding all glyphs that are outside + // the expected glyph range. + if (!hasID || !hasWidth || !hasHeight || !hasLeft || !hasTop || !hasAdvance || + glyph.metrics.width >= 256 || glyph.metrics.height >= 256 || + glyph.metrics.left < -128 || glyph.metrics.left >= 128 || + glyph.metrics.top < -128 || glyph.metrics.top >= 128 || + glyph.metrics.advance >= 256 || + glyph.id < glyphRange.first || glyph.id > glyphRange.second) { + continue; + } + // If the area of width/height is non-zero, we need to adjust the expected size // with the implicit border size, otherwise we expect there to be no bitmap at all. - const uint32_t expectedBitmapSize = - glyph.metrics.width && glyph.metrics.height - ? (glyph.metrics.width + 2 * SDFGlyph::borderSize) * - (glyph.metrics.height + 2 * SDFGlyph::borderSize) - : 0; + if (glyph.metrics.width && glyph.metrics.height) { + const Size size { + glyph.metrics.width + 2 * SDFGlyph::borderSize, + glyph.metrics.height + 2 * SDFGlyph::borderSize + }; - // Only treat this glyph as a correct glyph if it has all required fields, and if - // the bitmap has the correct length. It also needs to satisfy a few metrics conditions - // that ensure that the glyph isn't bogus. All other glyphs are malformed. - // We're also discarding all glyphs that are outside the expected glyph range. - if (hasID && hasWidth && hasHeight && hasLeft && hasTop && hasAdvance && - glyph.metrics.width < 256 && glyph.metrics.height < 256 && - glyph.metrics.left >= -128 && glyph.metrics.left < 128 && - glyph.metrics.top >= -128 && glyph.metrics.top < 128 && - glyph.metrics.advance < 256 && glyph.bitmap.size() == expectedBitmapSize && - glyph.id >= glyphRange.first && glyph.id <= glyphRange.second) { - glyphSet.insert(glyph.id, std::move(glyph)); + if (size.area() != glyphData.size()) { + continue; + } + + glyph.bitmap = AlphaImage(size, reinterpret_cast(glyphData.data()), glyphData.size()); } + + glyphSet.insert(glyph.id, std::move(glyph)); } } } diff --git a/test/fixtures/sprite_atlas/basic/expected.png b/test/fixtures/sprite_atlas/basic/expected.png index e886e060fe6dc7183d53125055380822f60422cd..cd13d16df6ca9819e2bacdf36184a3c53e5a050f 100644 GIT binary patch delta 671 zcmaFDv5j?tay?V6r;B4q#hkY{_GgI%iX8uF<8;p<-r*yo$QsupmlBjUIGQq4la?ka zG;P?}8PTDm!6nwF(A27?;GxhVq|0(}MX$=)yQ<2YZ^b+}e)A2+W+V|6FyGba4}>N`?dQo>!HWnTz?0Aob-m#D(g#{lx^=a)v$G^J71@! zyL7(V`s@B1d+W=3b^$g=JLC4u-G8fRjrX)Qs@MPAT`^Tbj11YVXaT0UvdqZ4-_6-(qypZR`MG29=Yi6+fy!{zif6`t$A3` z*_6_ltL3k=@20N>+U#9+d$VlR@yX|+y)%q=)la$7uPS}<)_DbBkaTzUuD&*J-8|ni z!@x!L*V6VcuHN(L#(AJWHx_Q4Xj}5pZ>#ogwWX$!@@Mz2J$>QhBI&X~!3HE(2Chg0QjEnx?oJHr&dI!FU|^B+ba4!+ zV0=3xIz!k|r0smz#20&ZJyCg=F7q`SO%aYRBJv_yT(uqg zU%A2@wL7G=;{>jBxaw$J+oqv(>q}GljTI}`a!Ee-*;H7b++@1A;~(?QnZ;?}=3CxB zcka!R1@=>ZB|p=0mf$|Q!0YDH>(_E;c*LCe@5S48UVQ7%`s%`rWpe+H9ZZvYz5W;P z_ulxVW%ho@3}1fV{W_-Xiouare+2hUIQ`zB^tI)YViTEz-|J1Zc@JCYo9?yEw-2~` zQ*77g9n*K5S(#B`=c_I2pPl|Y^VXA(r_?>x=*hgy4rqJ5DEhqW`twiJO$54UeNyj> z-=}=)I?K^}$2PpE%H`5_P5%4n$=5K$rB(ZHR4woU`}pCB8qI>D#Lw zg*^z}y4VoF4ubW0=Sj_u^8rtgYa`2vz@`uJ@eI^~dBc zT{W{#YraL_EbCn_f=&zHx?a(J?>p~`w3xhi9RZJf+fUsKw)I}ieQHB|$xGGJH;>7@R#lnJUS(9K}{3at_&B(webd~Pq%a3xo7(}b*`T6^Mo~ircY0@ z_8(evTwZU_iJg~^-VznD+&WdF!FFr;J|)FRFE7VsT=IT(* z>yLA0Rex(b_cmtI8lxtmD@kT+Vy4gAn}0X;`N6jHcRGdVT3uMte0K4@D_ZB@?@0e) zV0v}Cj;CH-lN0OWJz53dA6(fOv3OpzefVubo9>V5O9fteyiT1R{;6->_LwD)4CS^e zYdFXDBpf`UWSYG1_Z-!`ReoRZtl9D)(r?DH@&t{mCmz4`%AB_0{Io?yWi5}KAMNJi zt`(eZpB%V7?pPmN*c(`E OFnGH9xvX{ 0, 0, 0, 0 }), positions[65].rect); + // 'A' was not placed because not in the glyph set. + ASSERT_EQ(positions.end(), positions.find(65)); // 'B' was placed at the top left. ASSERT_NE(positions.end(), positions.find(66)); diff --git a/test/text/glyph_pbf.test.cpp b/test/text/glyph_pbf.test.cpp index 1e28dfbc3..be3ca3359 100644 --- a/test/text/glyph_pbf.test.cpp +++ b/test/text/glyph_pbf.test.cpp @@ -44,15 +44,17 @@ TEST(GlyphPBF, Parsing) { glyphAtlasObserver.glyphsLoaded = [&](const FontStack&, const GlyphRange&) { loop.stop(); - auto sdfs = glyphAtlas.getGlyphSet(fontStack)->getSDFs(); + const auto& sdfs = glyphAtlas.getGlyphSet(fontStack)->getSDFs(); // The fake glyphs don't contain a glyph that has the ID 0; it only contains glyphs with // undefined IDs, but the parser should remove them. EXPECT_TRUE(sdfs.size() == 1); EXPECT_TRUE(sdfs.find(69) != sdfs.end()); - auto& sdf = sdfs[69]; - EXPECT_EQ("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"s, sdf.bitmap); + auto& sdf = sdfs.at(69); + AlphaImage expected({7, 7}); + expected.fill('x'); + EXPECT_EQ(expected, sdf.bitmap); EXPECT_EQ(1u, sdf.metrics.width); EXPECT_EQ(1u, sdf.metrics.height); EXPECT_EQ(20, sdf.metrics.left); diff --git a/test/util/image.test.cpp b/test/util/image.test.cpp index b15ddc1b3..0cd4a7d8a 100644 --- a/test/util/image.test.cpp +++ b/test/util/image.test.cpp @@ -86,6 +86,35 @@ TEST(Image, WebPTile) { } #endif // !defined(__ANDROID__) && !defined(__APPLE__) && !defined(QT_IMAGE_DECODERS) +TEST(Image, Copy) { + PremultipliedImage src5({5, 5}); + PremultipliedImage dst5({5, 5}); + PremultipliedImage src10({10, 10}); + PremultipliedImage dst10({10, 10}); + + EXPECT_THROW(PremultipliedImage::copy(src5, dst10, {0, 0}, {0, 0}, {6, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src5, dst10, {0, 0}, {0, 0}, {0, 6}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src5, dst10, {1, 1}, {0, 0}, {5, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src5, dst10, {1, 1}, {0, 0}, {0, 5}), std::out_of_range); + + EXPECT_THROW(PremultipliedImage::copy(src10, dst5, {0, 0}, {0, 0}, {6, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst5, {0, 0}, {0, 0}, {0, 6}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst5, {0, 0}, {1, 1}, {5, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst5, {0, 0}, {1, 1}, {0, 5}), std::out_of_range); + + const uint32_t max = std::numeric_limits::max(); + + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {max, 0}, {0, 0}, {1, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, max}, {0, 0}, {0, 1}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, 0}, {max, 0}, {1, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, 0}, {0, max}, {0, 1}), std::out_of_range); + + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {1, 0}, {0, 0}, {max, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, 1}, {0, 0}, {0, max}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, 0}, {1, 0}, {max, 0}), std::out_of_range); + EXPECT_THROW(PremultipliedImage::copy(src10, dst10, {0, 0}, {0, 1}, {0, max}), std::out_of_range); +} + TEST(Image, Premultiply) { UnassociatedImage rgba({ 1, 1 }); rgba.data[0] = 255;