blob: 10d811e54981fcea81d48981cb05b068c2678e36 [file]
// 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/gif/writer.h"
#include <cstdint>
#include <optional>
#include <utility>
#include "absl/base/nullability.h"
#include "absl/functional/any_invocable.h"
#include "absl/log/die_if_null.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/notification.h"
#include "async/async_context.h"
#include "crypto/algorithms.h"
#include "formats/gif/block.h"
#include "formats/gif/embedder.h"
#include "formats/gif/hasher.h"
#include "generator/generator.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/generator.pb.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/writer.h"
namespace credentio {
namespace {
void AdjustDataHashExclusions(DataHashAssertion* absl_nonnull assertion,
int64_t c2pa_offset, int64_t c2pa_size) {
for (auto& exclusion : *assertion->mutable_exclusions()) {
if (exclusion.start() < c2pa_offset) {
// Before the C2PA segment, so no adjustment needed.
continue;
}
if (exclusion.start() == c2pa_offset) {
// This is the C2PA segment, just need to fill in the length.
exclusion.set_length(c2pa_size);
continue;
}
// This is after the C2PA segment, so we need to add the manifest size to
// the offset.
exclusion.set_start(exclusion.start() + c2pa_size);
}
}
absl::Status CopyAndEmbed(riegeli::Reader& input,
absl::string_view manifest_store,
int64_t insert_at_offset,
std::optional<int64_t> existing_c2pa_length,
riegeli::Writer& destination) {
if (!input.Seek(0)) {
return input.StatusOrAnnotate(
absl::DataLossError("failed to seek to start of input"));
}
GifEmbedder embedder;
ABSL_RETURN_IF_ERROR(
embedder.CopyAndEmbed(input, manifest_store, insert_at_offset,
existing_c2pa_length, destination));
return absl::OkStatus();
}
} // namespace
GifWriter::GifWriter(const ManifestStoreGenerator* absl_nonnull generator)
: generator_(*ABSL_DIE_IF_NULL(generator)) {}
absl::Status GifWriter::Write(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata) const {
absl::Notification done;
absl::Status status;
Write(std::move(request), source, destination, output_metadata,
AsyncContext(), [&](absl::Status s) {
status = std::move(s);
done.Notify();
});
done.WaitForNotification();
return status;
}
void GifWriter::Write(GenerateManifestStoreRequest request,
riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata,
AsyncContext context,
absl::AnyInvocable<void(absl::Status) &&> cb) const {
AsyncResolver<void> resolver(context, std::move(cb));
if (request.has_hard_binding()) {
resolver.Return(absl::InvalidArgumentError(
"request must not include `hard_binding` (binding is computed by "
"GifWriter)"));
return;
}
auto digest_result = DigestGif(*source, HashAlgorithm::kSha256);
if (!digest_result.ok()) {
resolver.Return(digest_result.status());
return;
}
if (digest_result->existing_c2pa_length.has_value() &&
(!request.has_actions() || !request.actions().has_opened())) {
resolver.Return(absl::AlreadyExistsError(
"C2PA chunk already exists, but no `opened` action was provided"));
return;
}
request.mutable_hard_binding()->mutable_data_hash()->Swap(
&digest_result->data_hash);
auto insertion_point = std::move(digest_result->insertion_point);
auto existing_c2pa_length = digest_result->existing_c2pa_length;
generator_.GenerateManifestStoreTemplate(
request, context,
[generator = &generator_, source, request,
insertion_point = std::move(insertion_point), existing_c2pa_length,
destination, output_metadata, context, resolver = std::move(resolver)](
absl::StatusOr<ManifestStoreTemplate> template_response) mutable {
if (!template_response.ok()) {
resolver.Return(template_response.status());
return;
}
uint64_t embedded_size = CalculateTotalGifC2paBlockSize(
template_response->manifest_store_size());
FinalizeManifestStoreRequest finalize_request;
finalize_request.mutable_manifest_store_template()->Swap(
&*template_response);
finalize_request.mutable_hard_binding()->MergeFrom(
request.hard_binding());
AdjustDataHashExclusions(
finalize_request.mutable_hard_binding()->mutable_data_hash(),
insertion_point.offset, embedded_size);
generator->FinalizeManifestStore(
finalize_request, context,
[source, insertion_point, existing_c2pa_length, destination,
output_metadata, resolver = std::move(resolver)](
absl::StatusOr<FinalizeManifestStoreResponse>
finalize_response) mutable {
if (!finalize_response.ok()) {
resolver.Return(finalize_response.status());
return;
}
auto copy_status = CopyAndEmbed(
*source, finalize_response->manifest_store(),
insertion_point.offset, existing_c2pa_length, *destination);
if (!copy_status.ok()) {
resolver.Return(std::move(copy_status));
return;
}
if (output_metadata != nullptr) {
output_metadata->timestamp_included =
finalize_response->is_timestamped();
}
resolver.Return(absl::OkStatus());
});
});
}
} // namespace credentio