| // Copyright 2026 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // | |
| #include "formats/jpeg/embedder.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/cord.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "constants/labels.h" | |
| #include "formats/jpeg/extractor.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/box_builder.h" | |
| #include "jumbf/constants.h" | |
| #include "jumbf/encode_params.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "riegeli/bytes/string_writer.h" | |
| #include "riegeli/endian/endian_writing.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::testing::AllOf; | |
| using ::testing::HasSubstr; | |
| absl::StatusOr<std::string> CreateFakeManifestStore(size_t content_size = 100) { | |
| jumbf::SuperBoxBuilder manifest_store_builder( | |
| kManifestStoreUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord(kManifestStoreLabel)}); | |
| ABSL_RETURN_IF_ERROR(manifest_store_builder.AddContent( | |
| jumbf::kBinaryDataBoxType, absl::Cord(std::string(content_size, 'x')))); | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox manifest_store_box, | |
| std::move(manifest_store_builder).Finalize()); | |
| return std::string(std::move(manifest_store_box).AsCord()); | |
| } | |
| struct JpegSegment { | |
| uint16_t marker; | |
| std::string payload; | |
| }; | |
| absl::StatusOr<std::string> CreateJpeg(absl::Span<const JpegSegment> segments) { | |
| std::string buffer; | |
| riegeli::StringWriter<> output(&buffer); | |
| for (auto segment : segments) { | |
| if (!riegeli::WriteBigEndian<uint16_t>(segment.marker, output)) { | |
| return output.status(); | |
| } | |
| if (!segment.payload.empty()) { | |
| uint16_t seg_size = sizeof(uint16_t) + segment.payload.size(); | |
| if (!riegeli::WriteBigEndian<uint16_t>(seg_size, output)) { | |
| return output.status(); | |
| } | |
| if (!output.Write(segment.payload)) { | |
| return output.status(); | |
| } | |
| } | |
| } | |
| output.Close(); | |
| return buffer; | |
| } | |
| TEST(JpegEmbedderTest, CopyAndEmbedAfterSOI) { | |
| auto input_s_or = CreateJpeg({{.marker = /*SOI*/ 0xffd8, .payload = ""}}); | |
| ABSL_ASSERT_OK(input_s_or); | |
| const auto& input_s = *input_s_or; | |
| riegeli::StringReader<absl::string_view> input(input_s); | |
| std::string output_s; | |
| riegeli::StringWriter output(&output_s); | |
| auto manifest_store_or = CreateFakeManifestStore(); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| const auto& manifest_store = *manifest_store_or; | |
| JpegEmbedder embedder(/*segment_id=*/30); | |
| ABSL_ASSERT_OK(embedder.CopyAndEmbed(input, manifest_store, | |
| /*insert_at_offset=*/2, output)); | |
| output.Close(); | |
| riegeli::StringReader<absl::string_view> output_reader(output_s); | |
| EXPECT_THAT(JpegExtractor().ExtractManifestStore(output_reader), | |
| IsOkAndHolds(manifest_store)); | |
| } | |
| TEST(JpegEmbedderTest, CopyAndEmbedBeforeSOS) { | |
| auto input_s_or = CreateJpeg({{.marker = /*SOI*/ 0xffd8, .payload = ""}, | |
| {.marker = /*SOS*/ 0xffda, .payload = "abcd"}, | |
| {.marker = /*EOI*/ 0xffd9, .payload = ""}}); | |
| ABSL_ASSERT_OK(input_s_or); | |
| const auto& input_s = *input_s_or; | |
| riegeli::StringReader input(input_s); | |
| std::string output_s; | |
| riegeli::StringWriter output(&output_s); | |
| auto manifest_store_or = CreateFakeManifestStore(); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| const auto& manifest_store = *manifest_store_or; | |
| JpegEmbedder embedder(/*segment_id=*/30); | |
| ABSL_ASSERT_OK(embedder.CopyAndEmbed(input, manifest_store, | |
| /*insert_at_offset=*/2, output)); | |
| output.Close(); | |
| riegeli::StringReader output_reader(output_s); | |
| EXPECT_THAT(JpegExtractor().ExtractManifestStore(output_reader), | |
| IsOkAndHolds(manifest_store)); | |
| } | |
| TEST(JpegEmbedderTest, CopyAndEmbedInAppSegments) { | |
| auto input_s_or = | |
| CreateJpeg({{.marker = /*SOI*/ 0xffd8, .payload = ""}, | |
| {.marker = /*APP0*/ 0xFFE0, .payload = "abcd"}, | |
| {.marker = /*APP13*/ 0xFFED, .payload = "efgh"}, | |
| {.marker = /*APP0*/ 0xFFE0, .payload = "ijkl"}}); | |
| ABSL_ASSERT_OK(input_s_or); | |
| const auto& input_s = *input_s_or; | |
| riegeli::StringReader input(input_s); | |
| std::string output_s; | |
| riegeli::StringWriter output(&output_s); | |
| auto manifest_store_or = CreateFakeManifestStore(); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| const auto& manifest_store = *manifest_store_or; | |
| JpegEmbedder embedder(/*segment_id=*/30); | |
| ABSL_ASSERT_OK(embedder.CopyAndEmbed(input, manifest_store, | |
| /*insert_at_offset=*/10, output)); | |
| output.Close(); | |
| EXPECT_THAT(output_s, | |
| AllOf(HasSubstr("abcd"), HasSubstr("efgh"), HasSubstr("ijkl"))); | |
| riegeli::StringReader output_reader(output_s); | |
| EXPECT_THAT(JpegExtractor().ExtractManifestStore(output_reader), | |
| IsOkAndHolds(manifest_store)); | |
| } | |
| } // namespace | |
| } // namespace credentio |