// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wallpaper/wallpaper_utils/sea_pen_metadata_utils.h"
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "ash/wallpaper/wallpaper_utils/sea_pen_utils_generated.h"
#include "ash/webui/common/mojom/sea_pen.mojom.h"
#include "ash/webui/common/mojom/sea_pen_generated.mojom.h"
#include "base/files/file_path.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_writer.h"
#include "base/json/values_util.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/values.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "third_party/re2/src/re2/re2.h"
namespace ash {
namespace {
// Converts a base::Value `time_value` into human-readable string representation
// of the date, such as "Dec 30, 2023". The string is translated into the user's
// current locale. Returns null on failure.
std::optional<std::u16string> GetCreationTimeInfo(
const base::Value& time_value) {
auto time = base::ValueToTime(time_value);
if (!time) {
DVLOG(2) << __func__ << " invalid time value received";
return std::nullopt;
}
return base::TimeFormatShortDate(*time);
}
std::optional<base::Value::Dict> AsOptionalDict(
data_decoder::DataDecoder::ValueOrError parsed) {
if (!parsed.has_value()) {
LOG(WARNING) << "Failed to parse JSON: " << parsed.error();
return std::nullopt;
}
if (!parsed->is_dict()) {
LOG(WARNING) << "Parsed JSON is not a dictionary";
return std::nullopt;
}
base::Value::Dict& dict = parsed->GetDict();
if (!dict.contains(kSeaPenFreeformQueryKey) &&
!dict.contains(kSeaPenTemplateIdKey)) {
LOG(WARNING) << "Parsed JSON does not contain required keys";
return std::nullopt;
}
return std::move(dict);
}
personalization_app::mojom::RecentSeaPenImageInfoPtr
SeaPenQueryDictToRecentImageInfo(
const std::optional<base::Value::Dict> query_dict) {
if (!query_dict.has_value()) {
DVLOG(2) << __func__ << " query_dict nullopt";
return nullptr;
}
auto* creation_time = query_dict->Find(kSeaPenCreationTimeKey);
if (!creation_time) {
DVLOG(2) << __func__
<< " missing creation time information in extracted data";
return nullptr;
}
auto* freeform_query = query_dict->FindString(kSeaPenFreeformQueryKey);
if (freeform_query) {
std::string unescaped_text;
base::UnescapeBinaryURLComponentSafe(
*freeform_query, /* fail_on_path_separators= */ false, &unescaped_text);
return personalization_app::mojom::RecentSeaPenImageInfo::New(
personalization_app::mojom::SeaPenQuery::NewTextQuery(unescaped_text),
GetCreationTimeInfo(*creation_time));
}
auto* template_id_ptr = query_dict->FindString(kSeaPenTemplateIdKey);
auto* option_dict = query_dict->FindDict(kSeaPenTemplateOptionsKey);
if (!template_id_ptr || !option_dict) {
DVLOG(2) << __func__ << " missing template information in extracted data";
return nullptr;
}
int template_id;
if (!base::StringToInt(*template_id_ptr, &template_id)) {
DVLOG(2) << __func__ << " invalid template id received";
return nullptr;
}
base::flat_map<personalization_app::mojom::SeaPenTemplateChip,
personalization_app::mojom::SeaPenTemplateOption>
options;
for (const auto [chip, option] : *option_dict) {
int chip_id, option_id;
if (!base::StringToInt(chip, &chip_id) ||
!base::StringToInt(option.GetString(), &option_id)) {
DVLOG(2) << __func__ << " invalid chip option received";
return nullptr;
}
options[static_cast<personalization_app::mojom::SeaPenTemplateChip>(
chip_id)] =
static_cast<personalization_app::mojom::SeaPenTemplateOption>(
option_id);
}
auto* user_visible_query_text =
query_dict->FindString(kSeaPenUserVisibleQueryTextKey);
auto* user_visible_query_template =
query_dict->FindString(kSeaPenUserVisibleQueryTemplateKey);
if (!user_visible_query_text || !user_visible_query_template) {
DVLOG(2) << __func__
<< " missing user visible query information in extracted data";
return nullptr;
}
personalization_app::mojom::SeaPenTemplateQueryPtr template_query =
personalization_app::mojom::SeaPenTemplateQuery::New(
static_cast<personalization_app::mojom::SeaPenTemplateId>(
template_id),
options,
personalization_app::mojom::SeaPenUserVisibleQuery::New(
*user_visible_query_text, *user_visible_query_template));
if (!IsValidTemplateQuery(template_query)) {
DVLOG(2) << __func__ << "invalid template query";
return nullptr;
}
return personalization_app::mojom::RecentSeaPenImageInfo::New(
personalization_app::mojom::SeaPenQuery::NewTemplateQuery(
std::move(template_query)),
GetCreationTimeInfo(*creation_time));
}
std::optional<int> ExtractTemplateIdFromSeaPenQueryDict(
const std::optional<base::Value::Dict> query_dict) {
if (!query_dict.has_value()) {
DVLOG(2) << __func__ << " query_dict nullopt";
return std::nullopt;
}
int template_id;
auto* template_id_ptr = query_dict->FindString(kSeaPenTemplateIdKey);
if (!template_id_ptr || !base::StringToInt(*template_id_ptr, &template_id)) {
return std::nullopt;
}
return template_id;
}
} // namespace
base::Value::Dict SeaPenQueryToDict(
const personalization_app::mojom::SeaPenQueryPtr& query) {
base::Value::Dict query_dict = base::Value::Dict();
query_dict.Set(kSeaPenCreationTimeKey, base::TimeToValue(base::Time::Now()));
switch (query->which()) {
case personalization_app::mojom::SeaPenQuery::Tag::kTextQuery:
query_dict.Set(kSeaPenFreeformQueryKey,
base::EscapeAllExceptUnreserved(query->get_text_query()));
break;
case personalization_app::mojom::SeaPenQuery::Tag::kTemplateQuery:
query_dict.Set(kSeaPenTemplateIdKey,
base::NumberToString(static_cast<int32_t>(
query->get_template_query()->id)));
base::Value::Dict options_dict = base::Value::Dict();
for (const auto& [chip, option] : query->get_template_query()->options) {
options_dict.Set(base::NumberToString(static_cast<int32_t>(chip)),
base::NumberToString(static_cast<int32_t>(option)));
}
query_dict.Set(kSeaPenTemplateOptionsKey, std::move(options_dict));
query_dict.Set(kSeaPenUserVisibleQueryTextKey,
query->get_template_query()->user_visible_query->text);
query_dict.Set(
kSeaPenUserVisibleQueryTemplateKey,
query->get_template_query()->user_visible_query->template_title);
break;
}
return query_dict;
}
std::string ExtractDcDescriptionContents(const std::string_view data) {
re2::RE2 tag_pattern("<dc:description>(.*)</dc:description>");
std::string result;
if (!re2::RE2::PartialMatch(data, tag_pattern, &result)) {
VLOG(0) << "Failed to find dc:description tag";
return std::string();
}
return result;
}
std::string QueryDictToXmpString(const base::Value::Dict& query_dict) {
static constexpr char kXmpData[] = R"(
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:description>%s</dc:description>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>)";
return base::StringPrintf(
kXmpData, base::WriteJson(query_dict).value_or(std::string()).c_str());
}
void DecodeJsonMetadata(
const std::string& json,
base::OnceCallback<
void(personalization_app::mojom::RecentSeaPenImageInfoPtr)> callback) {
data_decoder::DataDecoder::ParseJsonIsolated(
json, base::BindOnce(&AsOptionalDict)
.Then(base::BindOnce(&SeaPenQueryDictToRecentImageInfo))
.Then(std::move(callback)));
}
void DecodeJsonMetadataGetTemplateId(
const std::string& json,
base::OnceCallback<void(std::optional<int>)> callback) {
data_decoder::DataDecoder::ParseJsonIsolated(
json, base::BindOnce(&AsOptionalDict)
.Then(base::BindOnce(&ExtractTemplateIdFromSeaPenQueryDict))
.Then(std::move(callback)));
}
std::optional<uint32_t> GetIdFromFileName(const base::FilePath& file_path) {
const std::string name = file_path.BaseName().RemoveExtension().value();
uint32_t value;
if (base::StringToUint(name, &value)) {
return value;
}
LOG(WARNING) << "Invalid SeaPen file_path: " << file_path;
return std::nullopt;
}
std::vector<uint32_t> GetIdsFromFilePaths(
const std::vector<base::FilePath>& file_paths) {
std::vector<uint32_t> result;
for (const auto& file_path : file_paths) {
std::optional<uint32_t> id = GetIdFromFileName(file_path);
if (id.has_value()) {
result.push_back(id.value());
}
}
return result;
}
bool IsValidTemplateQuery(
const personalization_app::mojom::SeaPenTemplateQueryPtr& query) {
const auto query_id = query->id;
const auto query_options = query->options;
if (!TemplateToChipSet().contains(query_id)) {
LOG(WARNING) << "Template id not found.";
return false;
}
const auto chip_set = TemplateToChipSet().find(query_id)->second;
if (chip_set.size() != query_options.size()) {
LOG(WARNING) << "The chip size does not match the expected chip size.";
return false;
}
for (const auto& [query_chip, query_option] : query_options) {
if (!chip_set.contains(query_chip)) {
// The query chip is not in the template's chip set.
LOG(WARNING) << "Chip id is not found.";
return false;
}
const auto available_options = ChipToOptionSet().find(query_chip)->second;
if (!available_options.contains(query_option)) {
// The query's option is not an allowed option.
LOG(WARNING) << "Option id not found.";
return false;
}
}
return true;
}
std::string GetQueryString(
const personalization_app::mojom::RecentSeaPenImageInfoPtr& ptr) {
if (ptr.is_null() || ptr->query.is_null()) {
return std::string();
}
switch (ptr->query->which()) {
case personalization_app::mojom::SeaPenQuery::Tag::kTextQuery:
return ptr->query->get_text_query();
case personalization_app::mojom::SeaPenQuery::Tag::kTemplateQuery:
return ptr->query->get_template_query()->user_visible_query->text;
}
return std::string();
}
} // namespace ash