Offer standard web and print canvas formats
The canvas was two spin buttons, so "an Instagram story" or "A4 at 300 dpi" was a number the user had to work out. src/core/canvas.cpp now holds one table of standard sizes both front-ends read: a web format is a fixed pixel size, a print format a sheet in millimetres that becomes pixels at a dpi. Adding a format is a line in canvasFormats() — the dropdown, --format and --help all walk the table. The pixels stay the truth everywhere; renderChain never hears about formats. A format is the presentation of a canvas, carried alongside the pixels in Options and Project and empty for a custom size, and where one is named it decides the pixels — so a project saved as A4 reopens as A4 rather than as whatever it happened to measure. In the interface a format fills the width and height in and greys them out rather than hiding them: the size a sheet works out to is worth seeing. A print format also shows a DPI row, and Custom hands the spins back keeping the last size, which makes starting from a format and then tweaking it the ordinary path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#include "core/canvas.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
|
||||
#include "core/chain.hpp"
|
||||
|
||||
namespace effector {
|
||||
namespace {
|
||||
|
||||
// 210 mm reads as "210", 215.9 mm as "215.9". In the C locale whatever the
|
||||
// user's is: gtkmm sets the global locale from the environment, so a stream
|
||||
// left to its default writes "210,0" on a German desktop.
|
||||
std::string millimetres(double mm) {
|
||||
std::ostringstream out;
|
||||
out.imbue(std::locale::classic());
|
||||
out.setf(std::ios::fixed);
|
||||
out.precision(1);
|
||||
out << mm;
|
||||
std::string text = out.str();
|
||||
if (text.size() > 2 && text.compare(text.size() - 2, 2, ".0") == 0) text.resize(text.size() - 2);
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<CanvasFormat>& canvasFormats() {
|
||||
// Written out once and handed back by reference: the table is fixed, and
|
||||
// both front-ends walk it for their menu and their --help.
|
||||
static const std::vector<CanvasFormat> formats = {
|
||||
{"instagram-post", "Instagram post", false, 1080, 1080, 0.0, 0.0},
|
||||
{"instagram-portrait", "Instagram portrait", false, 1080, 1350, 0.0, 0.0},
|
||||
{"instagram-story", "Instagram story", false, 1080, 1920, 0.0, 0.0},
|
||||
{"hd", "Full HD", false, 1920, 1080, 0.0, 0.0},
|
||||
{"uhd", "4K UHD", false, 3840, 2160, 0.0, 0.0},
|
||||
{"a5", "A5", true, 0, 0, 148.0, 210.0},
|
||||
{"a4", "A4", true, 0, 0, 210.0, 297.0},
|
||||
{"a3", "A3", true, 0, 0, 297.0, 420.0},
|
||||
{"letter", "US Letter", true, 0, 0, 215.9, 279.4},
|
||||
};
|
||||
return formats;
|
||||
}
|
||||
|
||||
const CanvasFormat* canvasFormatByKey(const std::string& key) {
|
||||
if (key.empty()) return nullptr;
|
||||
for (const auto& format : canvasFormats()) {
|
||||
if (format.key == key) return &format;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void canvasFormatSize(const CanvasFormat& format, int dpi, int& width, int& height) {
|
||||
if (!format.print) {
|
||||
width = format.width;
|
||||
height = format.height;
|
||||
return;
|
||||
}
|
||||
const int clamped = std::clamp(dpi, kMinDpi, kMaxDpi);
|
||||
const auto pixels = [clamped](double mm) {
|
||||
const long value = std::lround(mm / 25.4 * clamped);
|
||||
return static_cast<int>(std::clamp<long>(value, kMinCanvasSide, kMaxCanvasSide));
|
||||
};
|
||||
width = pixels(format.widthMm);
|
||||
height = pixels(format.heightMm);
|
||||
}
|
||||
|
||||
std::string canvasFormatDetail(const CanvasFormat& format) {
|
||||
if (format.print) {
|
||||
return millimetres(format.widthMm) + " × " + millimetres(format.heightMm) + " mm";
|
||||
}
|
||||
return std::to_string(format.width) + " × " + std::to_string(format.height) + " px";
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace effector {
|
||||
|
||||
// A named canvas size both front-ends offer, so "A4 at 300 dpi" is one choice
|
||||
// rather than a pixel count the user has to work out. Web formats are a fixed
|
||||
// pixel size; print formats are a physical size in millimetres and only become
|
||||
// pixels once a resolution is given, which is why the dpi belongs to the
|
||||
// canvas rather than to any one format.
|
||||
struct CanvasFormat {
|
||||
std::string key; // written in a project and given to --format, e.g. "a4"
|
||||
std::string label; // shown in the interface, e.g. "A4"
|
||||
bool print = false;
|
||||
int width = 0; // pixels, web formats only
|
||||
int height = 0; //
|
||||
double widthMm = 0.0; // millimetres, print formats only
|
||||
double heightMm = 0.0;
|
||||
};
|
||||
|
||||
// The dpi a print format is rendered at. The bounds keep A3 — the largest
|
||||
// sheet here — inside kMaxCanvasSide at the top end, and keep a sheet from
|
||||
// coming out too small to see at the bottom.
|
||||
inline constexpr int kMinDpi = 36;
|
||||
inline constexpr int kMaxDpi = 1200;
|
||||
inline constexpr int kDefaultDpi = 300;
|
||||
|
||||
// In the order they are offered: the web formats first, then the paper sizes.
|
||||
const std::vector<CanvasFormat>& canvasFormats();
|
||||
|
||||
// Null when the key names no format, which is how a custom canvas — the user's
|
||||
// own pixel sizes — is spelled everywhere: an empty or unknown key.
|
||||
const CanvasFormat* canvasFormatByKey(const std::string& key);
|
||||
|
||||
// The pixel size of a format. `dpi` is ignored by a web format, which already
|
||||
// is a pixel size; a print format is rounded to the nearest pixel and clamped
|
||||
// to the canvas bounds.
|
||||
void canvasFormatSize(const CanvasFormat& format, int dpi, int& width, int& height);
|
||||
|
||||
// "1080 × 1080 px" or "210 × 297 mm", for the dropdown and for --help.
|
||||
std::string canvasFormatDetail(const CanvasFormat& format);
|
||||
|
||||
} // namespace effector
|
||||
+34
-2
@@ -10,6 +10,7 @@
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
|
||||
#include "core/canvas.hpp"
|
||||
#include "core/effect.hpp"
|
||||
#include "core/registry.hpp"
|
||||
|
||||
@@ -306,6 +307,18 @@ int canvasSide(const Json& value, const char* what) {
|
||||
return static_cast<int>(value.number);
|
||||
}
|
||||
|
||||
// Checked as a double before narrowing, exactly as a canvas side is.
|
||||
int canvasDpi(const Json& value) {
|
||||
if (value.kind != Json::Kind::Number || value.number != std::trunc(value.number)) {
|
||||
throw EffectError("the canvas dpi must be a whole number");
|
||||
}
|
||||
if (!(value.number >= kMinDpi) || !(value.number <= kMaxDpi)) {
|
||||
throw EffectError("the canvas dpi must be between " + std::to_string(kMinDpi) + " and " +
|
||||
std::to_string(kMaxDpi));
|
||||
}
|
||||
return static_cast<int>(value.number);
|
||||
}
|
||||
|
||||
std::string resolve(const std::string& path, const std::string& directory) {
|
||||
std::filesystem::path file(path);
|
||||
if (file.is_absolute() || directory.empty()) return path;
|
||||
@@ -323,8 +336,15 @@ std::string projectToJson(const Project& project, const std::string& directory)
|
||||
out.imbue(std::locale::classic());
|
||||
out << "{\n";
|
||||
out << " \"effector_project\": 1,\n";
|
||||
out << " \"canvas\": {\"width\": " << project.width << ", \"height\": " << project.height
|
||||
<< "},\n";
|
||||
out << " \"canvas\": {\"width\": " << project.width << ", \"height\": " << project.height;
|
||||
// The pixels are always written, so a project opens the same in a reader
|
||||
// that knows nothing of formats; the format and its dpi come after them
|
||||
// and only when there is one to name.
|
||||
if (const auto* format = canvasFormatByKey(project.format)) {
|
||||
out << ", \"format\": " << quote(format->key);
|
||||
if (format->print) out << ", \"dpi\": " << project.dpi;
|
||||
}
|
||||
out << "},\n";
|
||||
if (project.seed) out << " \"seed\": " << *project.seed << ",\n";
|
||||
|
||||
out << " \"sources\": [";
|
||||
@@ -396,6 +416,18 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
|
||||
if (const auto* height = canvas->member("height")) {
|
||||
project.height = canvasSide(*height, "height");
|
||||
}
|
||||
if (const auto* dpi = canvas->member("dpi")) project.dpi = canvasDpi(*dpi);
|
||||
// A format the build does not know is simply a custom canvas: the
|
||||
// pixels were written too, so the project still renders as it was
|
||||
// saved. Where the format is known it decides the pixels, so a
|
||||
// hand-edited dpi takes effect and a preset stays true to its name.
|
||||
if (const auto* name = canvas->member("format"); name != nullptr &&
|
||||
name->kind == Json::Kind::String) {
|
||||
if (const auto* format = canvasFormatByKey(name->text)) {
|
||||
project.format = format->key;
|
||||
canvasFormatSize(*format, project.dpi, project.width, project.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* seed = root.member("seed")) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/canvas.hpp"
|
||||
#include "core/chain.hpp"
|
||||
#include "core/params.hpp"
|
||||
|
||||
@@ -18,6 +19,12 @@ struct Project {
|
||||
std::vector<ChainStage> stages;
|
||||
int width = 1920;
|
||||
int height = 1080;
|
||||
// The named format the canvas came from, empty for a custom pixel size.
|
||||
// The pixels above stay the truth for the render; this is what the
|
||||
// interface puts back in its dropdown, and what a print format needs its
|
||||
// resolution for.
|
||||
std::string format;
|
||||
int dpi = kDefaultDpi;
|
||||
std::optional<std::uint64_t> seed;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user