ensure image script functions preserve metadata

This commit is contained in:
GenevensiS
2026-01-21 18:33:27 +01:00
parent c42068f918
commit 6b1c7488bf
18 changed files with 365 additions and 181 deletions
+2 -2
View File
@@ -180,13 +180,13 @@ int Style::update(Context& ctx) {
else if (automatic_side & AUTO_BOTTOM) bottom = top + height;
else {int tb = int(top + bottom); top = (tb - height) / 2; bottom = (tb + height) / 2; }
// adjust rotation point
if (angle != 0 && (automatic_side & (AUTO_LEFT | AUTO_TOP))) {
if (!almost_equal(angle, 0.0) && (automatic_side & (AUTO_LEFT | AUTO_TOP))) {
double s = sin(deg_to_rad(angle)), c = cos(deg_to_rad(angle));
if (automatic_side & AUTO_LEFT) { // attach right corner instead of left
left = left + width * (1 - c);
top = top + width * s;
}
if (automatic_side & AUTO_TOP) { // attach botom corner instead of top
if (automatic_side & AUTO_TOP) { // attach bottom corner instead of top
left = left - height * s;
top = top + height * (1 - c);
}
+34 -6
View File
@@ -126,12 +126,40 @@ public:
inline RealPoint getPos() const { return RealPoint(left, top); }
inline RealSize getSize() const { return RealSize(width, height); }
inline RealRect getExternalRect() const { return RealRect(left, top, width, height); }
inline std::string getExternalRectString(double scale, Radians angle, int offset_x, int offset_y, int img_width, int img_height) { ///< update the style before calling this
RealRect rect(left, top, width, height);
int degrees = lround(rad_to_deg(this->angle));
return transformAndEncodeRectInString(rect, degrees, scale, angle, offset_x, offset_y, img_width, img_height);
}
inline RealRect getCanonicalExternalRect() const {
if (almost_equal(angle, 90.0)) {
if (automatic_side & Style::AutomaticSide::AUTO_LEFT) {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left + width - height, top + height, height, width); //bottom right
else return RealRect(left + width, top, height, width); //top right
}
else {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left - height, top + height - width, height, width); //bottom left
else return RealRect(left, top - width, height, width); //top left
}
}
else if (almost_equal(angle, 270.0)) {
if (automatic_side & Style::AutomaticSide::AUTO_LEFT) {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left + width, top + height - width, height, width); //bottom right
else return RealRect(left + width - height, top - width, height, width); //top right
}
else {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left, top + height, height, width); //bottom left
else return RealRect(left - height, top, height, width); //top left
}
}
else if (almost_equal(angle, 180.0)) {
if (automatic_side & Style::AutomaticSide::AUTO_LEFT) {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left + width, top + height, width, height); //bottom right
else return RealRect(left + width, top - height, width, height); //top right
}
else {
if (automatic_side & Style::AutomaticSide::AUTO_TOP) return RealRect(left - width, top + height, width, height); //bottom left
else return RealRect(left - width, top - height, width, height); //top left
}
}
return getExternalRect();
}
/// Does this style have a non-zero size (or is it scripted)?
bool hasSize() const;
+7 -7
View File
@@ -39,6 +39,13 @@ public:
inline ImageValue(const ImageFieldP& field) : Value(field) {}
DECLARE_VALUE_TYPE(Image, LocalFileName);
inline Image getImage(const SetP& set) {
auto imageInputStream = set->openIn(filename);
Image img(*imageInputStream, wxBITMAP_TYPE_PNG);
if (!img.IsOk()) throw ScriptError(_ERROR_2_("file not found", filename.toStringForKey(), set));
return img;
}
ValueType filename; ///< Filename of the image (in the current package), or ""
Age last_update; ///< When was the image last changed?
};
@@ -55,11 +62,4 @@ public:
Scriptable<bool> store_in_metadata; ///< Is the image stored in full in the metadata when exporting?
int update(Context&) override;
inline std::string getExternalImageString(const SetP& set, ImageValue* value) { ///< update the style before calling this
auto imageInputStream = set->openIn(value->filename);
Image img(*imageInputStream, wxBITMAP_TYPE_PNG);
if (!img.IsOk()) throw ScriptError(_ERROR_2_("file not found", value->filename.toStringForKey(), set));
return encodeImageInString(img);
}
};
+8 -2
View File
@@ -89,12 +89,18 @@ FileFormatP mtg_editor_file_format();
// ----------------------------------------------------------------------------- : Other ways to export
/// Generate a wxImage of one or more cards
Image export_image(const SetP& set, const CardP& card, const bool write_metadata = true, const double zoom = 1.0, const Radians angle_radians = 0.0, const double bleed_pixels = 0.0);
Image export_image(const SetP& set, const vector<CardP>& cards, const int padding = 2, const double global_zoom = 1.0, const bool use_zoom_setting = true, const bool use_rotation_setting = true, const bool use_bleed_setting = false);
Image export_image(const SetP& set, const CardP& card, bool write_metadata = true, double zoom = 1.0, Radians angle_radians = 0.0, double bleed_pixels = 0.0);
Image export_image(const SetP& set, const vector<CardP>& cards, int padding = 2, double global_zoom = 1.0, bool use_zoom_setting = true, bool use_rotation_setting = true, bool use_bleed_setting = false);
/// Export the image of one or more cards to a given filename, using the app's zoom, rotation and bleed settings, and including metadata
void export_image(const SetP& set, const CardP& card, const String& filename);
void export_image(const SetP& set, const vector<CardP>& cards, const String& path, const String& filename_template, FilenameConflicts conflicts);
/// Write the metadata for a card
// Assuming first the zoom is applied, then the rotation, then the offset.
// This means that width and height need to be already scaled by a factor of zoom, but not already rotated
// while offset_x and offset_y need to be already scaled and already rotated.
String export_metadata(const SetP& set, const CardP& card, double zoom, Radians angle_radians, int width, int height, double offset_x, double offset_y);
/// Export a set to Magic Workstation format
void export_mws(Window* parent, const SetP& set);
+57 -65
View File
@@ -35,7 +35,7 @@ Rotation ZoomedUnrotatedDataViewer::getRotation() const {
// ----------------------------------------------------------------------------- : wxImage export
Image export_image(const SetP& set, const CardP& card, const bool write_metadata, const double zoom, const Radians angle_radians, const double bleed_pixels) {
Image export_image(const SetP& set, const CardP& card, bool write_metadata, double zoom, Radians angle_radians, double bleed_pixels) {
if (!set) throw Error(_("no set"));
/// create and zoom
ZoomedUnrotatedDataViewer viewer = ZoomedUnrotatedDataViewer(zoom);
@@ -129,47 +129,24 @@ Image export_image(const SetP& set, const CardP& card, const bool write_metadata
}
/// add metadata
if (write_metadata) {
String metadata = _("<mse-card-data>[");
IndexMap<FieldP, ValueP>& card_data = card->data;
boost::json::object cardv = mse_to_json(card, set.get());
boost::json::object& cardv_data = cardv["data"].as_object();
StyleSheetP stylesheet = set->stylesheetForP(card);
if (!settings.stylesheetSettingsFor(*stylesheet).card_notes_export()) cardv["notes"] = "";
// iterate over all image fields
for(IndexMap<FieldP, ValueP>::iterator it = card_data.begin() ; it != card_data.end() ; ++it) {
ImageValue* value = dynamic_cast<ImageValue*>(it->get());
if (value && !value->filename.empty()) {
FieldP field = (*it)->fieldP;
ImageStyle* style = dynamic_cast<ImageStyle*>(stylesheet->card_style.at(field->index).get());
if (style) {
style->update(set->getContext(card));
// store the entire image in the metadata
if (style->store_in_metadata()) {
std::string bytes = style->getExternalImageString(set, value);
cardv_data[field->name.ToStdString()] = bytes;
}
// store only crop coordinates
else {
std::string rect = style->getExternalRectString(zoom, angle_radians, bleed_pixels, bleed_pixels, width, height);
cardv_data[field->name.ToStdString()] = rect;
}
}
}
}
metadata += json_ugly_print(cardv) + _("]</mse-card-data>");
if (write_metadata) {
bool rotated = is_rad90(angle_radians) || is_rad270(angle_radians); // we stored width and height after rotation, but export_metadata expects them before rotation
String metadata = _("<mse-card-data>[")
+ export_metadata(set, card, zoom, angle_radians, rotated ? height : width, rotated ? width : height, bleed_pixels, bleed_pixels)
+ _("]</mse-card-data>");
img.SetOption(wxIMAGE_OPTION_PNG_DESCRIPTION, metadata);
}
return img;
}
Image export_image( const SetP& set, const vector<CardP>& cards,
const int padding,
const double global_zoom,
const bool use_zoom_setting,
const bool use_rotation_setting,
const bool use_bleed_setting) {
Image export_image(const SetP& set,
const vector<CardP>& cards,
int padding,
double global_zoom,
bool use_zoom_setting,
bool use_rotation_setting,
bool use_bleed_setting) {
if (!set) throw Error(_("no set"));
if (cards.size() == 0) throw Error(_("no cards"));
vector<Image> imgs;
@@ -224,32 +201,8 @@ Image export_image( const SetP& set, const vector<CardP>& cards,
for (int i = 0; i < cards.size(); ++i) {
if (i > 0) metadata += _(",");
CardP card = cards[i];
IndexMap<FieldP, ValueP>& card_data = card->data;
boost::json::object cardv = mse_to_json(card, set.get());
boost::json::object& cardv_data = cardv["data"].as_object();
StyleSheetP stylesheet = set->stylesheetForP(card);
if (!settings.stylesheetSettingsFor(*stylesheet).card_notes_export()) cardv["notes"] = "";
for(IndexMap<FieldP, ValueP>::iterator it = card_data.begin() ; it != card_data.end() ; ++it) {
ImageValue* value = dynamic_cast<ImageValue*>(it->get());
if (value && !value->filename.empty()) {
FieldP field = (*it)->fieldP;
ImageStyle* style = dynamic_cast<ImageStyle*>(stylesheet->card_style.at(field->index).get());
if (style) {
style->update(set->getContext(card));
// store the entire image in the metadata
if (style->store_in_metadata()) {
std::string bytes = style->getExternalImageString(set, value);
cardv_data[field->name.ToStdString()] = bytes;
}
// store only crop coordinates
else {
std::string rect = style->getExternalRectString(zooms[i], angles[i], bleeds[i] + offsets[i], bleeds[i], widths[i], heights[i]);
cardv_data[field->name.ToStdString()] = rect;
}
}
}
}
metadata += json_ugly_print(cardv);
bool rotated = is_rad90(angles[i]) || is_rad270(angles[i]); // we stored width and height after rotation, but export_metadata expects them before rotation
metadata += export_metadata(set, card, zooms[i], angles[i], rotated ? heights[i] : widths[i], rotated ? widths[i] : heights[i], bleeds[i] + offsets[i], bleeds[i]);
}
metadata += _("]</mse-card-data>");
global_img.SetOption(wxIMAGE_OPTION_PNG_DESCRIPTION, metadata);
@@ -265,8 +218,7 @@ void export_image(const SetP& set, const CardP& card, const String& filename) {
img.SaveFile(filename);
}
void export_image(const SetP& set, const vector<CardP>& cards,
const String& path, const String& filename_template, FilenameConflicts conflicts)
void export_image(const SetP& set, const vector<CardP>& cards, const String& path, const String& filename_template, FilenameConflicts conflicts)
{
wxBusyCursor busy;
// Script
@@ -289,4 +241,44 @@ void export_image(const SetP& set, const vector<CardP>& cards,
used.insert(filename);
export_image(set, card, filename);
}
}
}
String export_metadata(const SetP& set, const CardP& card, double zoom, Radians angle_radians, int width, int height, double offset_x, double offset_y)
{
IndexMap<FieldP, ValueP>& card_data = card->data;
boost::json::object cardv = mse_to_json(card, set.get());
boost::json::object& cardv_data = cardv["data"].as_object();
StyleSheetP stylesheet = set->stylesheetForP(card);
if (!settings.stylesheetSettingsFor(*stylesheet).card_notes_export()) cardv["notes"] = "";
RealRect bounds_rect = RealRect(0, 0, width, height);
int bounds_degrees = 0;
RealRect::rotate(bounds_rect, bounds_degrees, width, height, lround(rad_to_deg(angle_radians)));
RealRect::translate(bounds_rect, bounds_degrees, offset_x, offset_y);
cardv.emplace("bounds", encodeRectInStdString(bounds_rect, bounds_degrees));
// iterate over all image fields
for (IndexMap<FieldP, ValueP>::iterator it = card_data.begin(); it != card_data.end(); ++it) {
ImageValue* value = dynamic_cast<ImageValue*>(it->get());
if (value && !value->filename.empty()) {
FieldP field = (*it)->fieldP;
ImageStyle* style = dynamic_cast<ImageStyle*>(stylesheet->card_style.at(field->index).get());
if (style) {
style->update(set->getContext(card));
// store the entire image in the metadata
if (style->store_in_metadata()) {
Image img = value->getImage(set);
cardv_data[field->name.ToStdString()] = encodeImageInString(img);
}
// store only crop coordinates
else {
RealRect rect = style->getCanonicalExternalRect();
int degrees = lround(style->angle());
RealRect::scale(rect, degrees, zoom, zoom);
RealRect::rotate(rect, degrees, width, height, lround(rad_to_deg(angle_radians))); // width and height are already scaled
RealRect::translate(rect, degrees, offset_x, offset_y); // offset_x and offset_y are already scaled and rotated
cardv_data[field->name.ToStdString()] = encodeRectInStdString(rect, degrees);
}
}
}
}
return json_ugly_print(cardv);
}
+61 -38
View File
@@ -9,13 +9,15 @@
// ----------------------------------------------------------------------------- : Includes
#include <util/prec.hpp>
#include <util/real_point.hpp>
#include <boost/json.hpp>
#include <wx/filename.h>
#include <fstream>
// ----------------------------------------------------------------------------- : Crop Rect Encoding
/// Encode a rect in a std::string
inline static std::string encodeRectInStdString(wxRect rect, int degrees) {
inline static std::string encodeRectInStdString(RealRect rect, int degrees) {
return "<mse-crop-data>" + std::to_string((int)std::ceil (rect.x)) +
";" + std::to_string((int)std::ceil (rect.y)) +
";" + std::to_string((int)std::floor(rect.width)) +
@@ -25,7 +27,7 @@ inline static std::string encodeRectInStdString(wxRect rect, int degrees) {
}
/// Encode a rect in a wxString
inline static String encodeRectInWxString(wxRect rect, int degrees) {
inline static String encodeRectInWxString(RealRect rect, int degrees) {
return _("<mse-crop-data>") + wxString::Format(wxT("%i"), (int)std::ceil (rect.x)) +
_(";") + wxString::Format(wxT("%i"), (int)std::ceil (rect.y)) +
_(";") + wxString::Format(wxT("%i"), (int)std::floor(rect.width)) +
@@ -35,7 +37,7 @@ inline static String encodeRectInWxString(wxRect rect, int degrees) {
}
/// Retreive a rect encoded in a string, return true if successful
inline static bool decodeRectFromString(const String& rectString, wxRect& rect_out, int& degrees_out) {
inline static bool decodeRectFromString(const String& rectString, RealRect& rect_out, int& degrees_out) {
size_t start = rectString.find(_("<mse-crop-data>"));
if (start == String::npos) return false;
size_t end = rectString.find(_("</mse-crop-data>"), start + 15);
@@ -73,44 +75,22 @@ inline static bool decodeRectFromString(const String& rectString, wxRect& rect_o
if(!string.ToInt(&degrees_out)) return false;
rect_out = wxRect(x, y, width, height);
return true;
}
/// Apply a transformation to a rect, return true if successful
inline static bool transformEncodedRect(wxRect& rect, int& degrees, double scale, Radians angle, int offset_x, int offset_y, int img_width, int img_height) {
if (degrees != 0 && degrees != 90 && degrees != 180 && degrees != 270) return false;
rect = wxRect(rect.x * scale, rect.y * scale, rect.width * scale, rect.height * scale);
if (is_rad0(angle)) {
} else if (is_rad180(angle)) {
rect = wxRect(img_width - rect.x - rect.width, img_height - rect.y - rect.height, rect.width, rect.height);
degrees += 180;
} else if (is_rad90(angle)) {
rect = wxRect(rect.y, img_height - rect.x - rect.width, rect.height, rect.width);
degrees += 90;
} else if (is_rad270(angle)) {
rect = wxRect(img_width - rect.y - rect.height, rect.x, rect.height, rect.width);
degrees += 270;
} else {
return false;
}
rect = wxRect(rect.x + offset_x, rect.y + offset_y, rect.width, rect.height);
if (degrees >= 360) degrees -= 360;
rect_out = RealRect(x, y, width, height);
return true;
}
/// Retreive a rect encoded in a string, apply a transformation, then encode it back
inline static String transformEncodedRect(const String& rectString, double scale, Radians angle, int offset_x, int offset_y, int img_width, int img_height) {
wxRect rect;
inline static String transformEncodedRect(const String& rectString, RectTransform transform, double param_x, double param_y, int mode) {
RealRect rect(0,0,0,0);
int degrees;
if (!decodeRectFromString(rectString, rect, degrees)) return _("");
if (!transformEncodedRect(rect, degrees, scale, angle, offset_x, offset_y, img_width, img_height)) return _("");
transform(rect, degrees, param_x, param_y, mode);
return encodeRectInWxString(rect, degrees);
}
/// Retreive all rects encoded in a string, apply a transformation, then encode them back
inline static String transformAllEncodedRects(const String& rectString, double scale, Radians angle, int offset_x, int offset_y, int img_width, int img_height) {
wxRect rect;
inline static String transformAllEncodedRects(const String& rectString, RectTransform transform, double param_x, double param_y, int mode = 0) {
RealRect rect(0,0,0,0);
int degrees;
size_t start = rectString.find(_("<mse-crop-data>"));
if (start == String::npos) return rectString;
@@ -121,19 +101,13 @@ inline static String transformAllEncodedRects(const String& rectString, double s
end = rectString.find(_("</mse-crop-data>"), start + 15);
if (end == String::npos) return rectString;
end += 16;
result = result + transformEncodedRect(rectString.substr(start, end - start), scale, angle, offset_x, offset_y, img_width, img_height);
result = result + transformEncodedRect(rectString.substr(start, end - start), transform, param_x, param_y, mode);
start = rectString.find(_("<mse-crop-data>"), end);
}
result = result + rectString.substr(end);
return result;
}
/// Apply a transformation to a rect, then encode it in a string
inline static std::string transformAndEncodeRectInString(wxRect rect, int degrees, double scale, Radians angle, int offset_x, int offset_y, int img_width, int img_height) {
if (!transformEncodedRect(rect, degrees, scale, angle, offset_x, offset_y, img_width, img_height)) return "";
return encodeRectInStdString(rect, degrees);
}
// ----------------------------------------------------------------------------- : File to UTF8 Encoding
/// Encode a file in a string
@@ -227,3 +201,52 @@ inline static Image decodeImageFromString(const String& string) {
wxRemoveFile(temppath.substr(0, temppath.size() - 4));
return img;
}
// ----------------------------------------------------------------------------- : Metadata manipulation
inline static String metadata_merge(const Image& img1, const Image& img2, int offset_x1 = 0, int offset_y1 = 0, int offset_x2 = 0, int offset_y2 = 0)
{
if (img1.HasOption(wxIMAGE_OPTION_PNG_DESCRIPTION)) {
String metadata1 = img1.GetOption(wxIMAGE_OPTION_PNG_DESCRIPTION);
if (offset_x1 != 0 || offset_y1 != 0) metadata1 = transformAllEncodedRects(metadata1, RealRect::translate, offset_x1, offset_y1);
if (img2.HasOption(wxIMAGE_OPTION_PNG_DESCRIPTION)) {
String metadata2 = img2.GetOption(wxIMAGE_OPTION_PNG_DESCRIPTION);
if (offset_x2 != 0 || offset_y2 != 0) metadata2 = transformAllEncodedRects(metadata2, RealRect::translate, offset_x2, offset_y2);
size_t end1 = metadata1.find(_("</mse-card-data>"));
size_t start2 = metadata2.find(_("<mse-card-data>"));
if (end1 != String::npos && start2 != String::npos && end1 > 0 && start2 + 16 < metadata2.size()) {
metadata1 = metadata1.substr(0, end1 - 1) + "," + metadata2.substr(start2 + 16);
}
}
return metadata1;
}
else if (img2.HasOption(wxIMAGE_OPTION_PNG_DESCRIPTION)) {
String metadata2 = img2.GetOption(wxIMAGE_OPTION_PNG_DESCRIPTION);
if (offset_x2 != 0 || offset_y2 != 0) metadata2 = transformAllEncodedRects(metadata2, RealRect::translate, offset_x2, offset_y2);
return metadata2;
}
return _("");
}
inline static boost::json::array metadata_to_json(const String& metadata) {
size_t start = metadata.find(_("<mse-card-data>"));
if (start == String::npos) return boost::json::array();
size_t end = metadata.find(_("</mse-card-data>"), start + 15);
if (end == String::npos) return boost::json::array();
String string = metadata.substr(start + 15, end - (start + 15));
try {
boost::system::error_code ec;
boost::json::parse_options options;
options.allow_invalid_utf8 = true;
boost::json::value jv = boost::json::parse(string.ToStdString(), ec, {}, options);
if(ec || !jv.is_array()) {
queue_message(MESSAGE_ERROR, _ERROR_("json cant parse"));
return boost::json::array();
}
return jv.as_array();
}
catch (...) {
queue_message(MESSAGE_ERROR, _ERROR_("json cant parse"));
return boost::json::array();
}
}