blob: ce513f6a5bdc4f95e30e405e08ee684010d09311 [file] [edit]
// 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/png/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/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/notification.h"
#include "async/async_context.h"
#include "crypto/algorithms.h"
#include "formats/png/constants.h"
#include "formats/png/embedder.h"
#include "formats/png/hasher.h"
#include "formats/png/reader.h"
#include "generator/generator.h"
#include "proto/generator.pb.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/writer.h"
namespace credentio {
PngWriter::PngWriter(const ManifestStoreGenerator* absl_nonnull generator,
Options options)
: generator_(*ABSL_DIE_IF_NULL(generator)), options_(std::move(options)) {}
absl::Status PngWriter::Write(GenerateManifestStoreRequest request,
riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata) {
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 PngWriter::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) {
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 "
"PngWriter)"));
return;
}
// DigestPng() takes an optional processor over (reader, chunk), so bind the
// request, which stays alive for the duration of the digest below.
auto exclusion_fn = [this, &request](riegeli::Reader& reader,
const PngChunk& chunk) {
return options_.exclusion_processor(reader, chunk, request);
};
std::optional<PngExclusionProcessor> exclusion_processor = std::nullopt;
if (options_.exclusion_processor != nullptr) {
exclusion_processor.emplace(std::move(exclusion_fn));
}
// Hashes the source and reports where the manifest store chunk goes. The
// hash covers the asset as it will look once that chunk is in place: the
// chunk itself is recorded as a zero-length exclusion for now, and is sized
// below once the manifest store template tells us how big it will be.
auto digest_result =
DigestPng(*source, HashAlgorithm::kSha256, exclusion_processor);
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() =
digest_result->assertion;
// The manifest store covers its own size, so it is generated in two phases:
// a template reserves the space, then the exclusions are fixed up to account
// for it, and only then is the manifest store finalized and embedded.
generator_.GenerateManifestStoreTemplate(
request, context,
[this, source, request, digest_result = std::move(*digest_result),
destination, output_metadata, context, resolver = std::move(resolver)](
absl::StatusOr<ManifestStoreTemplate> template_response) mutable {
if (!template_response.ok()) {
resolver.Return(template_response.status());
return;
}
constexpr int64_t kMaxManifestStoreSizeBytes =
100 * 1024 * 1024; // 100 MiB
if (template_response->manifest_store_size() <= 0 ||
template_response->manifest_store_size() >
kMaxManifestStoreSizeBytes) {
resolver.Return(absl::InvalidArgumentError(
absl::StrCat("invalid manifest store template size: ",
template_response->manifest_store_size())));
return;
}
FinalizeManifestStoreRequest finalize_request;
*finalize_request.mutable_manifest_store_template() =
std::move(*template_response);
*finalize_request.mutable_hard_binding()->mutable_data_hash() =
request.hard_binding().data_hash();
// Size of the whole `caBX` chunk record that will be inserted.
int64_t embedded_size =
finalize_request.manifest_store_template().manifest_store_size() +
kPngChunkLengthSize + kPngChunkTypeC2pa.size() + kPngCrcSize;
// The exclusions were recorded against the source asset, but they must
// describe the output asset. Inserting the chunk shifts everything
// after the insertion point by the size it adds (the size of the new
// chunk, less any existing `caBX` chunk it replaces), and the
// placeholder exclusion at the insertion point becomes the chunk.
for (auto& exclusion : *finalize_request.mutable_hard_binding()
->mutable_data_hash()
->mutable_exclusions()) {
if (exclusion.start() > digest_result.insertion_offset) {
exclusion.set_start(exclusion.start() + embedded_size -
digest_result.existing_c2pa_length.value_or(0));
} else if (exclusion.start() == digest_result.insertion_offset) {
exclusion.set_length(embedded_size);
}
}
generator_.FinalizeManifestStore(
finalize_request, context,
[source, digest_result = std::move(digest_result), destination,
output_metadata, resolver = std::move(resolver)](
absl::StatusOr<FinalizeManifestStoreResponse>
finalize_response) mutable {
if (!finalize_response.ok()) {
resolver.Return(finalize_response.status());
return;
}
if (output_metadata != nullptr) {
output_metadata->timestamp_included =
finalize_response->is_timestamped();
}
PngEmbedder embedder;
auto status = embedder.CopyAndEmbed(
*source, finalize_response->manifest_store(), digest_result,
*destination);
resolver.Return(status);
});
});
}
} // namespace credentio