From 32c5393fba07f003510a79fa847c53bb34ff9ecb Mon Sep 17 00:00:00 2001 From: Jacob Trimble Date: Mon, 5 Oct 2020 12:15:11 -0700 Subject: [PATCH] Add helpers for DVB-sub colors. Issue #832 Change-Id: I6350306c7d9a6450d82994bbd9a9a239986bc3fa --- packager/media/formats/dvb/dvb.gyp | 39 ++ packager/media/formats/dvb/dvb_image.cc | 269 ++++++++++++++ packager/media/formats/dvb/dvb_image.h | 137 +++++++ .../media/formats/dvb/dvb_image_unittest.cc | 335 ++++++++++++++++++ packager/packager.gyp | 2 + 5 files changed, 782 insertions(+) create mode 100644 packager/media/formats/dvb/dvb.gyp create mode 100644 packager/media/formats/dvb/dvb_image.cc create mode 100644 packager/media/formats/dvb/dvb_image.h create mode 100644 packager/media/formats/dvb/dvb_image_unittest.cc diff --git a/packager/media/formats/dvb/dvb.gyp b/packager/media/formats/dvb/dvb.gyp new file mode 100644 index 0000000000..fd85d91c73 --- /dev/null +++ b/packager/media/formats/dvb/dvb.gyp @@ -0,0 +1,39 @@ +# Copyright 2020 Google LLC. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +{ + 'variables': { + 'shaka_code': 1, + }, + 'targets': [ + { + 'target_name': 'dvb', + 'type': '<(component)', + 'sources': [ + 'dvb_image.cc', + 'dvb_image.h', + ], + 'dependencies': [ + '../../base/media_base.gyp:media_base', + '../../../third_party/libpng/libpng.gyp:libpng', + ], + }, + { + 'target_name': 'dvb_unittest', + 'type': '<(gtest_target_type)', + 'sources': [ + 'dvb_image_unittest.cc', + ], + 'dependencies': [ + '../../../testing/gtest.gyp:gtest', + '../../../testing/gmock.gyp:gmock', + '../../event/media_event.gyp:mock_muxer_listener', + '../../test/media_test.gyp:media_test_support', + 'dvb', + ] + }, + ], +} diff --git a/packager/media/formats/dvb/dvb_image.cc b/packager/media/formats/dvb/dvb_image.cc new file mode 100644 index 0000000000..c6155e6879 --- /dev/null +++ b/packager/media/formats/dvb/dvb_image.cc @@ -0,0 +1,269 @@ +// Copyright 2020 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/dvb/dvb_image.h" + +#include +#include +#include + +#include "packager/base/logging.h" + +namespace shaka { +namespace media { + +namespace { + +// See ETSI EN 300 743 Section 9.1. +constexpr const uint8_t k4To2ReductionMap[] = { + 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, + 0x2, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, +}; + +// The only time when A==0 is when it is transparent. This means we can use +// other values internally for special values. +constexpr const RgbaColor kNoColor{145, 92, 47, 0}; + +// DVB uses transparency, but libpng uses alpha, so we need to reverse the T +// value so we can pass the value to libpng. +#define COLOR(r, g, b, t) \ + RgbaColor { \ + static_cast(255 * (r) / 100), \ + static_cast(255 * (g) / 100), \ + static_cast(255 * (b) / 100), \ + static_cast(255 * (100 - t) / 100) \ + } +// Default color maps see ETSI EN 300 743 Section 10. +constexpr const RgbaColor k2BitDefaultColors[] = { + COLOR(0, 0, 0, 100), // 0 = 0b00 + COLOR(100, 100, 100, 0), // 1 = 0b01 + COLOR(0, 0, 0, 0), // 2 = 0b10 + COLOR(50, 50, 50, 0), // 3 = 0b11 +}; +// Default color maps see ETSI EN 300 743 Section 10. +constexpr const RgbaColor k4BitDefaultColors[] = { + COLOR(0, 0, 0, 100), // 0 = 0b0000 + COLOR(100, 0, 0, 0), // 1 = 0b0001 + COLOR(0, 100, 0, 0), // 2 = 0b0010 + COLOR(100, 100, 0, 0), // 3 = 0b0011 + COLOR(0, 0, 100, 0), // 4 = 0b0100 + COLOR(100, 0, 100, 0), // 5 = 0b0101 + COLOR(0, 100, 100, 0), // 6 = 0b0110 + COLOR(100, 100, 100, 0), // 7 = 0b0111 + + COLOR(0, 0, 0, 0), // 8 = 0b1000 + COLOR(50, 0, 0, 0), // 9 = 0b1001 + COLOR(0, 50, 0, 0), // 10 = 0b1010 + COLOR(50, 50, 0, 0), // 11 = 0b1011 + COLOR(0, 0, 50, 0), // 12 = 0b1100 + COLOR(50, 0, 50, 0), // 13 = 0b1101 + COLOR(0, 50, 50, 0), // 14 = 0b1110 + COLOR(50, 50, 50, 0), // 15 = 0b1111 +}; + +#define GET_BIT(n) ((entry_id >> (8 - (n))) & 0x1) +// Default color maps see ETSI EN 300 743 Section 10. +RgbaColor Get8BitDefaultColor(uint8_t entry_id) { + uint8_t r, g, b, t; + if (entry_id == 0) { + return COLOR(0, 0, 0, 100); + } else if ((entry_id & 0xf8) == 0) { + r = 100 * GET_BIT(8); + g = 100 * GET_BIT(7); + b = 100 * GET_BIT(6); + t = 75; + } else if (!GET_BIT(1)) { + r = (33 * GET_BIT(8)) + (67 * GET_BIT(4)); + g = (33 * GET_BIT(7)) + (67 * GET_BIT(3)); + b = (33 * GET_BIT(6)) + (67 * GET_BIT(2)); + t = GET_BIT(5) ? 50 : 0; + } else { + r = (17 * GET_BIT(8)) + (33 * GET_BIT(4)) + (GET_BIT(5) ? 0 : 50); + g = (17 * GET_BIT(7)) + (33 * GET_BIT(3)) + (GET_BIT(5) ? 0 : 50); + b = (17 * GET_BIT(6)) + (33 * GET_BIT(2)) + (GET_BIT(5) ? 0 : 50); + t = 0; + } + return COLOR(r, g, b, t); +} +#undef GET_BIT +#undef COLOR + +} // namespace + +DvbImageColorSpace::DvbImageColorSpace() { + for (auto& item : color_map_2_) + item = kNoColor; + for (auto& item : color_map_4_) + item = kNoColor; + for (auto& item : color_map_8_) + item = kNoColor; +} + +DvbImageColorSpace::~DvbImageColorSpace() {} + +RgbaColor DvbImageColorSpace::GetColor(BitDepth bit_depth, + uint8_t entry_id) const { + auto color = GetColorRaw(bit_depth, entry_id); + if (color != kNoColor) + return color; + + // If we don't have the exact bit-depth, try mapping to another bit-depth. + // See ETSI EN 300 743 Section 9. + RgbaColor default_color, alt1, alt2; + switch (bit_depth) { + case BitDepth::k2Bit: + DCHECK_LT(entry_id, 4u); + alt1 = GetColorRaw(BitDepth::k4Bit, bit_depth_2_to_4_[entry_id]); + alt2 = GetColorRaw(BitDepth::k8Bit, bit_depth_2_to_8_[entry_id]); + default_color = k2BitDefaultColors[entry_id]; + break; + case BitDepth::k4Bit: + DCHECK_LT(entry_id, 16u); + alt1 = GetColorRaw(BitDepth::k8Bit, bit_depth_4_to_8_[entry_id]); + alt2 = GetColorRaw(BitDepth::k2Bit, k4To2ReductionMap[entry_id]); + default_color = k4BitDefaultColors[entry_id]; + break; + case BitDepth::k8Bit: + // 8-to-4-bit reduction is just take the low bits. + alt1 = GetColorRaw(BitDepth::k4Bit, entry_id & 0xf); + alt2 = GetColorRaw(BitDepth::k2Bit, k4To2ReductionMap[entry_id & 0xf]); + default_color = Get8BitDefaultColor(entry_id); + break; + default: + // Windows can't detect that all enums are handled and doesn't like + // NOTREACHED. + return kNoColor; + } + + if (alt1 != kNoColor) + return alt1; + if (alt2 != kNoColor) + return alt2; + return default_color; +} + +void DvbImageColorSpace::SetColor(BitDepth bit_depth, + uint8_t entry_id, + RgbaColor color) { + DCHECK(color != kNoColor); + switch (bit_depth) { + case BitDepth::k2Bit: + DCHECK_LT(entry_id, 4u); + color_map_2_[entry_id] = color; + break; + case BitDepth::k4Bit: + DCHECK_LT(entry_id, 16u); + color_map_4_[entry_id] = color; + break; + case BitDepth::k8Bit: + color_map_8_[entry_id] = color; + break; + } +} + +void DvbImageColorSpace::Set2To4BitDepthMap(const uint8_t* map) { + memcpy(bit_depth_2_to_4_, map, sizeof(bit_depth_2_to_4_)); +} + +void DvbImageColorSpace::Set2To8BitDepthMap(const uint8_t* map) { + memcpy(bit_depth_2_to_8_, map, sizeof(bit_depth_2_to_8_)); +} + +void DvbImageColorSpace::Set4To8BitDepthMap(const uint8_t* map) { + memcpy(bit_depth_4_to_8_, map, sizeof(bit_depth_4_to_8_)); +} + +RgbaColor DvbImageColorSpace::GetColorRaw(BitDepth bit_depth, + uint8_t entry_id) const { + switch (bit_depth) { + case BitDepth::k2Bit: + return color_map_2_[entry_id]; + case BitDepth::k4Bit: + return color_map_4_[entry_id]; + case BitDepth::k8Bit: + return color_map_8_[entry_id]; + } + // Not reached, but Windows doesn't like NOTREACHED. + return kNoColor; +} + +DvbImageBuilder::DvbImageBuilder(const DvbImageColorSpace* color_space, + const RgbaColor& default_color, + uint16_t max_width, + uint16_t max_height) + : pixels_(new RgbaColor[max_width * max_height]), + color_space_(color_space), + top_pos_{0, 0}, + bottom_pos_{0, 1}, // Skip top row for bottom row. + max_width_(max_width), + max_height_(max_height), + width_(0) { + for (size_t i = 0; i < static_cast(max_width) * max_height; i++) + pixels_[i] = default_color; +} + +DvbImageBuilder::~DvbImageBuilder() {} + +bool DvbImageBuilder::AddPixel(BitDepth bit_depth, + uint8_t byte_code, + bool is_top_rows) { + auto& pos = is_top_rows ? top_pos_ : bottom_pos_; + if (pos.x >= max_width_ || pos.y >= max_height_) { + LOG(ERROR) << "DVB-sub image cannot fit in region/window"; + return false; + } + + pixels_[pos.y * max_width_ + pos.x++] = + color_space_->GetColor(bit_depth, byte_code); + if (pos.x > width_) + width_ = pos.x; + return true; +} + +void DvbImageBuilder::NewRow(bool is_top_rows) { + auto& pos = is_top_rows ? top_pos_ : bottom_pos_; + pos.x = 0; + pos.y += 2; // Skip other row. +} + +void DvbImageBuilder::MirrorToBottomRows() { + for (size_t line = 0; line < max_height_ - 1u; line += 2) { + std::memcpy(&pixels_[(line + 1) * max_width_], &pixels_[line * max_width_], + max_width_ * sizeof(RgbaColor)); + } + bottom_pos_ = top_pos_; + if (max_height_ % 2 == 0) + bottom_pos_.y++; + else + bottom_pos_.y--; // Odd-height images don't end in odd-row, so move back. +} + +bool DvbImageBuilder::GetPixels(const RgbaColor** pixels, + uint16_t* width, + uint16_t* height) const { + size_t max_y, min_y; + std::tie(min_y, max_y) = std::minmax(top_pos_.y, bottom_pos_.y); + if (max_y == 1 || max_y != min_y + 1) { + // 1. We should have at least one row. + // 2. Both top-rows and bottom-rows should have the same number of rows. + LOG(ERROR) << "Incomplete DVB-sub image"; + return false; + } + + *width = width_; + // We skipped the other row in NewRow, so rollback. + *height = static_cast(max_y - 1); + *pixels = pixels_.get(); + if (*height > max_height_) { + LOG(ERROR) << "DVB-sub image cannot fit in region/window"; + return false; + } + + return true; +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/formats/dvb/dvb_image.h b/packager/media/formats/dvb/dvb_image.h new file mode 100644 index 0000000000..725954bab8 --- /dev/null +++ b/packager/media/formats/dvb/dvb_image.h @@ -0,0 +1,137 @@ +// Copyright 2020 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_MEDIA_DVB_DVB_IMAGE_H_ +#define PACKAGER_MEDIA_DVB_DVB_IMAGE_H_ + +#include +#include + +namespace shaka { +namespace media { + +struct RgbaColor { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + + bool operator==(const RgbaColor& other) const { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + bool operator!=(const RgbaColor& other) const { return !(*this == other); } +}; +// To avoid copying, we pass an RgbaColor array as a uint8_t* pointer to libpng +// for RGBA. +static_assert(std::is_pod::value, "RgbaColor must be POD"); +static_assert(sizeof(RgbaColor) == 4, "RgbaColor not packed correctly"); + +enum class BitDepth : uint8_t { + k2Bit, + k4Bit, + k8Bit, +}; + +/// Defines a color-space for DVB-sub images. This maps to a single CLUT in the +/// spec. This holds a map of the byte codes to the respective RGB colors. +/// This also handles getting the default colors when none are provided and +/// converting between bit-depths if applicable. +/// +/// When handling bit-depths, this will attempt to use the bit-depth provided +/// before converting upward then downward. Each color is only set for that +/// specific bit-depth; meaning different bit-depths can have different colors +/// mapped to the same byte-code. +class DvbImageColorSpace { + public: + DvbImageColorSpace(); + ~DvbImageColorSpace(); + + DvbImageColorSpace(const DvbImageColorSpace&) = delete; + DvbImageColorSpace& operator=(const DvbImageColorSpace&) = delete; + + RgbaColor GetColor(BitDepth bit_depth, uint8_t entry_id) const; + + void SetColor(BitDepth bit_depth, uint8_t entry_id, RgbaColor color); + /// Must pass a 4-element array; elements are copied over. + void Set2To4BitDepthMap(const uint8_t* map); + /// Must pass a 4-element array; elements are copied over. + void Set2To8BitDepthMap(const uint8_t* map); + /// Must pass a 16-element array; elements are copied over. + void Set4To8BitDepthMap(const uint8_t* map); + + private: + RgbaColor GetColorRaw(BitDepth bit_depth, uint8_t entry_id) const; + + // These hold the colors for each entry ID. Each value is initialized to the + // special value kNoColor meaning there isn't a value present. + RgbaColor color_map_2_[4]; + RgbaColor color_map_4_[16]; + RgbaColor color_map_8_[256]; + // See ETSI EN 300 743 Sections 10.4, 10.5, 10.6 for defaults. + uint8_t bit_depth_2_to_4_[4] = {0x0, 0x7, 0x8, 0xf}; + uint8_t bit_depth_2_to_8_[4] = {0x0, 0x77, 0x88, 0xff}; + uint8_t bit_depth_4_to_8_[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff}; +}; + +/// Defines a builder that generates an image from a DVB-sub byte stream. This +/// allocates a single buffer big enough to hold the max-sized image and fills +/// it in series. The NewRow() method must be called to start a new line of the +/// image. +/// +/// This adds pixels in an interlaced format. Adding pixels and new rows on +/// top-rows doesn't affect the bottom-rows. Top-rows refers to even-indexed +/// lines (e.g. 0,2,4). +class DvbImageBuilder { + public: + DvbImageBuilder(const DvbImageColorSpace* color_space, + const RgbaColor& default_color, + uint16_t max_width, + uint16_t max_height); + ~DvbImageBuilder(); + + DvbImageBuilder(const DvbImageBuilder&) = delete; + DvbImageBuilder& operator=(const DvbImageBuilder&) = delete; + + uint16_t max_width() const { return max_width_; } + uint16_t max_height() const { return max_height_; } + + bool AddPixel(BitDepth bit_depth, uint8_t byte_code, bool is_top_rows); + void NewRow(bool is_top_rows); + /// Copies the top-rows to the bottom rows. + void MirrorToBottomRows(); + + /// Gets the pixel buffer. Each row is based on the max_width field, but + /// the max filled row with will be given in `width`. This assumes that + /// NewRow was called recently and we are at the beginning of the rows. + /// + /// @param pixels A pointer to a RgbaColor* variable that will be set to the + /// pixel data pointer. + /// @param width Will be filled with the max width of all rows. + /// @param height Will be filled with the number of rows set. + /// @return True on success, false on error. + bool GetPixels(const RgbaColor** pixels, + uint16_t* width, + uint16_t* height) const; + + private: + struct Position { + uint16_t x, y; + }; + + const std::unique_ptr pixels_; + const DvbImageColorSpace* const color_space_; + Position top_pos_, bottom_pos_; + const uint16_t max_width_; + const uint16_t max_height_; + uint16_t width_; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_DVB_DVB_IMAGE_H_ diff --git a/packager/media/formats/dvb/dvb_image_unittest.cc b/packager/media/formats/dvb/dvb_image_unittest.cc new file mode 100644 index 0000000000..cdb9be586d --- /dev/null +++ b/packager/media/formats/dvb/dvb_image_unittest.cc @@ -0,0 +1,335 @@ +// Copyright 2020 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/dvb/dvb_image.h" + +#include +#include + +#include "packager/media/base/rcheck.h" + +namespace shaka { +namespace media { + +namespace { + +// Use an unusual alpha value to avoid being equal to a default color. +const RgbaColor kRed{255, 0, 0, 211}; +const RgbaColor kGreen{0, 255, 0, 211}; +const RgbaColor kBlue{0, 0, 255, 211}; +const RgbaColor kBlack{0, 0, 0, 211}; +const RgbaColor kWhite{255, 255, 255, 211}; +const RgbaColor kYellow{255, 255, 0, 211}; +const uint8_t kRedId = 0; +const uint8_t kGreenId = 1; +const uint8_t kBlueId = 2; +const uint8_t kBlackId = 3; +const uint8_t kWhiteId = 4; +const uint8_t kYellowId = 5; + +const bool kTopRow = true; +const bool kBottomRow = false; + +void FillDefaultColorSpace(DvbImageColorSpace* space) { + for (auto depth : {BitDepth::k2Bit, BitDepth::k4Bit, BitDepth::k8Bit}) { + space->SetColor(depth, kRedId, kRed); + space->SetColor(depth, kGreenId, kGreen); + space->SetColor(depth, kBlueId, kBlue); + space->SetColor(depth, kBlackId, kBlack); + if (depth != BitDepth::k2Bit) { + space->SetColor(depth, kWhiteId, kWhite); + space->SetColor(depth, kYellowId, kYellow); + } + } +} + +bool AddPixelRow(DvbImageBuilder* image, + uint16_t width, + uint8_t color_id, + bool is_top_rows) { + for (size_t i = 0; i < width; i++) + RCHECK(image->AddPixel(BitDepth::k8Bit, color_id, is_top_rows)); + image->NewRow(is_top_rows); + return true; +} + +void CheckImagePixels(const DvbImageBuilder* image, + uint16_t width, + std::initializer_list rows) { + uint16_t actual_width, height; + const RgbaColor* pixels = nullptr; + ASSERT_TRUE(image->GetPixels(&pixels, &actual_width, &height)); + ASSERT_EQ(actual_width, width); + ASSERT_EQ(height, rows.size()); + + size_t row = 0; + for (const auto& color : rows) { + for (size_t i = 0; i < width; i++) + EXPECT_EQ(pixels[image->max_width() * row + i], color); + row++; + } +} + +} // namespace + +TEST(DvbImageColorSpaceTest, GetsColors) { + DvbImageColorSpace space; + space.SetColor(BitDepth::k8Bit, 0, kRed); + space.SetColor(BitDepth::k8Bit, 1, kGreen); + space.SetColor(BitDepth::k8Bit, 2, kBlue); + + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0), kRed); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 1), kGreen); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 2), kBlue); +} + +TEST(DvbImageColorSpaceTest, BitDepthsAreDifferent) { + DvbImageColorSpace space; + space.SetColor(BitDepth::k8Bit, 0, kRed); + space.SetColor(BitDepth::k8Bit, 1, kGreen); + space.SetColor(BitDepth::k4Bit, 0, kBlue); + space.SetColor(BitDepth::k4Bit, 1, kBlack); + space.SetColor(BitDepth::k2Bit, 0, kWhite); + space.SetColor(BitDepth::k2Bit, 1, kYellow); + + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0), kRed); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 1), kGreen); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0), kBlue); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 1), kBlack); + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0), kWhite); + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 1), kYellow); +} + +TEST(DvbImageColorSpaceTest, HandlesBitDepthReduction) { + DvbImageColorSpace space; + space.SetColor(BitDepth::k2Bit, 0x0, kRed); + space.SetColor(BitDepth::k2Bit, 0x1, kGreen); + space.SetColor(BitDepth::k4Bit, 0x1, kWhite); + space.SetColor(BitDepth::k4Bit, 0x5, kBlue); + space.SetColor(BitDepth::k4Bit, 0x7, kBlack); + space.SetColor(BitDepth::k4Bit, 0x9, kYellow); + + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x00), kRed); // 0x0 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x02), kGreen); // 0x1 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x72), kGreen); // 0x1 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x35), kBlue); // 0x5 in 4-bit + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x17), kBlack); // 0x7 in 4-bit + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x09), kYellow); // Exact match + + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x0), kRed); // 0x0 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x2), kGreen); // 0x1 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x3), kGreen); // 0x1 in 2-bit + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x1), kWhite); // Exact match +} + +TEST(DvbImageColorSpaceTest, HandlesBitDepthExpansion) { + DvbImageColorSpace space; + space.SetColor(BitDepth::k2Bit, 0x0, kRed); + space.SetColor(BitDepth::k4Bit, 0x7, kGreen); + space.SetColor(BitDepth::k4Bit, 0x8, kBlue); + space.SetColor(BitDepth::k8Bit, 0x11, kBlack); + space.SetColor(BitDepth::k8Bit, 0xff, kYellow); + + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x0), kRed); // Exact match + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x1), kGreen); // 0x07 in 4-bit + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x3), kYellow); // 0xff in 8-bit + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x7), kGreen); // Exact match + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x1), kBlack); // 0x11 in 8-bit +} + +TEST(DvbImageColorSpaceTest, HandlesCustomBitDepthExpansion) { + const uint8_t k2To4Map[] = {0x0, 0x6, 0x7, 0x0}; + const uint8_t k2To8Map[] = {0x0, 0xa, 0xb, 0x0}; + const uint8_t k4To8Map[] = {0x0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; + DvbImageColorSpace space; + space.Set2To4BitDepthMap(k2To4Map); + space.Set2To8BitDepthMap(k2To8Map); + space.Set4To8BitDepthMap(k4To8Map); + space.SetColor(BitDepth::k2Bit, 0x0, kRed); + space.SetColor(BitDepth::k4Bit, 0x0, kGreen); + space.SetColor(BitDepth::k4Bit, 0x6, kBlue); + space.SetColor(BitDepth::k8Bit, 0x0, kBlack); + space.SetColor(BitDepth::k8Bit, 0xb, kWhite); + space.SetColor(BitDepth::k8Bit, 0x12, kYellow); + + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x0), kRed); // Exact match + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x1), kBlue); // 0x06 in 4-bit + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x2), kWhite); // 0xb in 8-bit + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x0), kGreen); // Exact match + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x1), kYellow); // 0x12 in 8-bit +} + +TEST(DvbImageColorSpaceTest, HandlesDefaultColors) { + DvbImageColorSpace space; + + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x0).a, 0u); // Only T is spec'd + EXPECT_EQ(space.GetColor(BitDepth::k2Bit, 0x2), (RgbaColor{0, 0, 0, 255})); + + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x0).a, 0u); // Only T is spec'd + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x1), (RgbaColor{255, 0, 0, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x2), (RgbaColor{0, 255, 0, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x5), + (RgbaColor{255, 0, 255, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0x9), (RgbaColor{127, 0, 0, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k4Bit, 0xa), (RgbaColor{0, 127, 0, 255})); + + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x0).a, 0u); // Only T is spec'd + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x1), (RgbaColor{255, 0, 0, 63})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x3), (RgbaColor{255, 255, 0, 63})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x61), + (RgbaColor{84, 170, 170, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x46), + (RgbaColor{0, 84, 255, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0x1a), + (RgbaColor{170, 84, 0, 127})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0xf2), + (RgbaColor{211, 255, 211, 255})); + EXPECT_EQ(space.GetColor(BitDepth::k8Bit, 0xbe), + (RgbaColor{84, 127, 43, 255})); +} + +TEST(DvbImageBuilderTest, BasicFlow) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth, 5); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kGreenId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kWhiteId, kTopRow)); + + ASSERT_TRUE(AddPixelRow(&image, kWidth, kBlueId, kBottomRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kBlackId, kBottomRow)); + + CheckImagePixels(&image, kWidth, {kRed, kBlue, kGreen, kBlack, kWhite}); +} + +TEST(DvbImageBuilderTest, AllowsSmallerImages) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth + 10, 5); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kGreenId, kBottomRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kBlueId, kTopRow)); + + CheckImagePixels(&image, kWidth, {kRed, kGreen, kBlue}); +} + +TEST(DvbImageBuilderTest, ValidatesMaxWidth) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth, 5); + for (size_t i = 0; i < kWidth; i++) + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + // Cannot exceed max_width on first line. + ASSERT_FALSE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + // Despite the error, the image should still be in the same state as before. + image.NewRow(kTopRow); + for (size_t i = 0; i < kWidth; i++) + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + // Cannot exceed max_width on other lines. + ASSERT_FALSE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); +} + +TEST(DvbImageBuilderTest, SupportsInconsistentWidths) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + + DvbImageBuilder image(&colors, kBlack, 10, 10); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + image.NewRow(kTopRow); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kBlueId, kBottomRow)); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kBlueId, kBottomRow)); + image.NewRow(kBottomRow); + ASSERT_TRUE(image.AddPixel(BitDepth::k8Bit, kYellowId, kTopRow)); + image.NewRow(kTopRow); + + const RgbaColor* pixels; + uint16_t width, height; + ASSERT_TRUE(image.GetPixels(&pixels, &width, &height)); + EXPECT_EQ(width, 3); + EXPECT_EQ(height, 3); + + EXPECT_EQ(pixels[0], kRed); + EXPECT_EQ(pixels[1], kRed); + EXPECT_EQ(pixels[2], kRed); + EXPECT_EQ(pixels[10], kBlue); + EXPECT_EQ(pixels[11], kBlue); + EXPECT_EQ(pixels[12], kBlack); + EXPECT_EQ(pixels[20], kYellow); + EXPECT_EQ(pixels[21], kBlack); + EXPECT_EQ(pixels[22], kBlack); +} + +TEST(DvbImageBuilderTest, ValidatesTotalLength) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth, 3); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kBottomRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + + ASSERT_FALSE(image.AddPixel(BitDepth::k8Bit, kRedId, kTopRow)); + ASSERT_FALSE(image.AddPixel(BitDepth::k8Bit, kRedId, kBottomRow)); +} + +TEST(DvbImageBuilderTest, ValidatesTopBottomFieldsMatch) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth, 5); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kBottomRow)); + // Not enough bottom rows. + + uint16_t width, height; + const RgbaColor* pixels; + ASSERT_FALSE(image.GetPixels(&pixels, &width, &height)); +} + +TEST(DvbImageBuilderTest, MirrorToBottomRowsEven) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + const uint16_t kHeight = 4; + + DvbImageBuilder image(&colors, kBlack, kWidth, kHeight); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kGreenId, kTopRow)); + image.MirrorToBottomRows(); + + CheckImagePixels(&image, kWidth, {kRed, kRed, kGreen, kGreen}); +} + +TEST(DvbImageBuilderTest, MirrorToBottomRowsOdd) { + DvbImageColorSpace colors; + FillDefaultColorSpace(&colors); + const uint16_t kWidth = 4; + const uint16_t kHeight = 5; + + DvbImageBuilder image(&colors, kBlack, kWidth, kHeight); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kRedId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kGreenId, kTopRow)); + ASSERT_TRUE(AddPixelRow(&image, kWidth, kBlueId, kTopRow)); + image.MirrorToBottomRows(); + + CheckImagePixels(&image, kWidth, {kRed, kRed, kGreen, kGreen, kBlue}); +} + +} // namespace media +} // namespace shaka diff --git a/packager/packager.gyp b/packager/packager.gyp index bce4c3a653..8b11f0bf2c 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -35,6 +35,7 @@ 'media/crypto/crypto.gyp:crypto', 'media/demuxer/demuxer.gyp:demuxer', 'media/event/media_event.gyp:media_event', + 'media/formats/dvb/dvb.gyp:dvb', 'media/formats/mp2t/mp2t.gyp:mp2t', 'media/formats/mp4/mp4.gyp:mp4', 'media/formats/packed_audio/packed_audio.gyp:packed_audio', @@ -213,6 +214,7 @@ 'media/crypto/crypto.gyp:crypto_unittest', 'media/demuxer/demuxer.gyp:demuxer_unittest', 'media/event/media_event.gyp:media_event_unittest', + 'media/formats/dvb/dvb.gyp:dvb_unittest', 'media/formats/mp2t/mp2t.gyp:mp2t_unittest', 'media/formats/mp4/mp4.gyp:mp4_unittest', 'media/formats/packed_audio/packed_audio.gyp:packed_audio_unittest',