Templates for render output paths
This adds basic templating support to render output paths. By putting
"{variable_name}" in the path string, it will be replaced by the named
variable's value when generating the actual output path. This is similar
to how "//" is already substituted with the path to the blend file's
current directory.
This templating system is implemented for both the primary render output
path as well as the File Output node in the compositing nodes. Support
for using templates in other places can be implemented in future PRs.
In addition to the "{variable_name}" syntax, some additional syntax is
also supported:
- Since "{" and "}" now have special meaning, "{{" and "}}" are now
escape sequences for literal "{" and "}".
- "{variable_name:format_specifier}", where "format_specifier" is a
special syntax using "#", which allows the user to specify how numeric
variables should be formatted:
- "{variable_name:###}" will format the number as an integer with at
least 3 characters (padding with zeros as needed).
- "{variable_name:.##}" will format the number as a float with
precisely 2 fractional digits.
- "{variable_name:###.##}" will format the number as a float with at
least 3 characters for the integer part and precisely 2 for the
fractional part.
For the primary render output path: if there is a template syntax error,
a variable doesn't exist, or a format specifier isn't valid (e.g. trying
to format a string with "##"), the render that needs to write to the
output path fails with a descriptive error message.
For both the primary and File Output node paths: if there are template
syntax errors the field is highlighted in red in the UI, and a tooltip
describes the offending syntax errors. Note that these do *not* yet
reflect errors due to missing variables. That will be for a follow-up
PR.
In addition to the general system, this PR also implements a limited set
of variables for use in templates, but more can be implemented in future
PRs. The variables added in this PR are:
- `blend_name`: the name of the current blend file without the file
extension.
- `fps`: the frames per second of the current scene.
- `resolution_x` and `resolution_y`: the render output resolution.
Pull Request: https://projects.blender.org/blender/blender/pulls/134860
This commit is contained in:
committed by
Nathan Vegdahl
parent
d648ffb0f1
commit
e0beb7afe6
@@ -278,6 +278,7 @@ class InfoPropertyRNA:
|
||||
"is_readonly",
|
||||
"is_never_none",
|
||||
"is_path_supports_blend_relative",
|
||||
"is_path_supports_templates",
|
||||
)
|
||||
global_lookup = {}
|
||||
|
||||
@@ -303,6 +304,7 @@ class InfoPropertyRNA:
|
||||
self.is_never_none = rna_prop.is_never_none
|
||||
self.is_argument_optional = rna_prop.is_argument_optional
|
||||
self.is_path_supports_blend_relative = rna_prop.is_path_supports_blend_relative
|
||||
self.is_path_supports_templates = rna_prop.is_path_supports_templates
|
||||
|
||||
self.type = rna_prop.type.lower()
|
||||
fixed_type = getattr(rna_prop, "fixed_type", "")
|
||||
@@ -483,6 +485,9 @@ class InfoPropertyRNA:
|
||||
if self.is_path_supports_blend_relative:
|
||||
type_info.append("blend relative ``//`` prefix supported")
|
||||
|
||||
if self.is_path_supports_templates:
|
||||
type_info.append("template expressions like \"{blend_name}\" are supported")
|
||||
|
||||
if type_info:
|
||||
type_str += ", ({:s})".format(", ".join(type_info))
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
/* Blender file format version. */
|
||||
#define BLENDER_FILE_VERSION BLENDER_VERSION
|
||||
#define BLENDER_FILE_SUBVERSION 66
|
||||
#define BLENDER_FILE_SUBVERSION 67
|
||||
|
||||
/* Minimum Blender version that supports reading file written with the current
|
||||
* version. Older Blender versions will test this and cancel loading the file, showing a warning to
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "BKE_path_templates.hh"
|
||||
|
||||
struct BlendDataReader;
|
||||
struct BlendWriter;
|
||||
struct ID;
|
||||
@@ -17,6 +19,7 @@ struct ImbFormatOptions;
|
||||
struct ImageFormatData;
|
||||
struct ImBuf;
|
||||
struct Scene;
|
||||
struct RenderData;
|
||||
|
||||
/* Init/Copy/Free */
|
||||
|
||||
@@ -36,22 +39,33 @@ void BKE_image_format_set(ImageFormatData *imf, ID *owner_id, const char imtype)
|
||||
|
||||
/* File Paths */
|
||||
|
||||
void BKE_image_path_from_imformat(char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
int frame,
|
||||
const ImageFormatData *im_format,
|
||||
bool use_ext,
|
||||
bool use_frames,
|
||||
const char *suffix);
|
||||
void BKE_image_path_from_imtype(char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
int frame,
|
||||
char imtype,
|
||||
bool use_ext,
|
||||
bool use_frames,
|
||||
const char *suffix);
|
||||
/**
|
||||
* \param template_variables: the map of variables to use for template
|
||||
* substitution. Optional: if null, template substitution will not be performed.
|
||||
*
|
||||
* \return If any template errors are encountered, returns those errors. On
|
||||
* success, returns an empty Vector.
|
||||
*/
|
||||
blender::Vector<blender::bke::path_templates::Error> BKE_image_path_from_imformat(
|
||||
char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
const blender::bke::path_templates::VariableMap *template_variables,
|
||||
int frame,
|
||||
const ImageFormatData *im_format,
|
||||
bool use_ext,
|
||||
bool use_frames,
|
||||
const char *suffix);
|
||||
blender::Vector<blender::bke::path_templates::Error> BKE_image_path_from_imtype(
|
||||
char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
const blender::bke::path_templates::VariableMap *template_variables,
|
||||
int frame,
|
||||
char imtype,
|
||||
bool use_ext,
|
||||
bool use_frames,
|
||||
const char *suffix);
|
||||
|
||||
/**
|
||||
* The number of extensions an image may have (`.jpg`, `.jpeg` for example).
|
||||
|
||||
229
source/blender/blenkernel/BKE_path_templates.hh
Normal file
229
source/blender/blenkernel/BKE_path_templates.hh
Normal file
@@ -0,0 +1,229 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
*
|
||||
* \brief Functions and classes for applying templates with variable expressions
|
||||
* to filepaths.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_string_utils.hh"
|
||||
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
namespace blender::bke::path_templates {
|
||||
|
||||
/**
|
||||
* Variables (names and associated values) for use in template substitution.
|
||||
*
|
||||
* Note that this is not intended to be persistent storage, but rather is
|
||||
* transient for collecting data that is relevant/available in a given
|
||||
* templating context.
|
||||
*
|
||||
* There are currently three supported variable types: string, integer, and
|
||||
* float. Names must be unique across all types: you can't have a string *and*
|
||||
* integer both with the name "bob".
|
||||
*/
|
||||
class VariableMap {
|
||||
blender::Map<std::string, std::string> strings_;
|
||||
blender::Map<std::string, int64_t> integers_;
|
||||
blender::Map<std::string, double> floats_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Check if a variable of the given name exists.
|
||||
*/
|
||||
bool contains(blender::StringRef name) const;
|
||||
|
||||
/**
|
||||
* Remove the variable with the given name.
|
||||
*
|
||||
* \return True if the variable existed and was removed, false if it didn't
|
||||
* exist in the first place.
|
||||
*/
|
||||
bool remove(blender::StringRef name);
|
||||
|
||||
/**
|
||||
* Add a string variable with the given name and value.
|
||||
*
|
||||
* If there is already a variable with that name, regardless of type, the new
|
||||
* variable is *not* added (no overwriting).
|
||||
*
|
||||
* \return True if the variable was successfully added, false if there was
|
||||
* already a variable with that name.
|
||||
*/
|
||||
bool add_string(blender::StringRef name, blender::StringRef value);
|
||||
|
||||
/**
|
||||
* Add an integer variable with the given name and value.
|
||||
*
|
||||
* If there is already a variable with that name, regardless of type, the new
|
||||
* variable is *not* added (no overwriting).
|
||||
*
|
||||
* \return True if the variable was successfully added, false if there was
|
||||
* already a variable with that name.
|
||||
*/
|
||||
bool add_integer(blender::StringRef name, int64_t value);
|
||||
|
||||
/**
|
||||
* Add a float variable with the given name and value.
|
||||
*
|
||||
* If there is already a variable with that name, regardless of type, the new
|
||||
* variable is *not* added (no overwriting).
|
||||
*
|
||||
* \return True if the variable was successfully added, false if there was
|
||||
* already a variable with that name.
|
||||
*/
|
||||
bool add_float(blender::StringRef name, double value);
|
||||
|
||||
/**
|
||||
* Fetch the value of the string variable with the given name.
|
||||
*
|
||||
* \return The value if a string variable with that name exists, nullopt
|
||||
* otherwise.
|
||||
*/
|
||||
std::optional<blender::StringRefNull> get_string(blender::StringRef name) const;
|
||||
|
||||
/**
|
||||
* Fetch the value of the integer variable with the given name.
|
||||
*
|
||||
* \return The value if a integer variable with that name exists, nullopt
|
||||
* otherwise.
|
||||
*/
|
||||
std::optional<int64_t> get_integer(blender::StringRef name) const;
|
||||
|
||||
/**
|
||||
* Fetch the value of the float variable with the given name.
|
||||
*
|
||||
* \return The value if a float variable with that name exists, nullopt
|
||||
* otherwise.
|
||||
*/
|
||||
std::optional<double> get_float(blender::StringRef name) const;
|
||||
};
|
||||
|
||||
enum class ErrorType {
|
||||
UNESCAPED_CURLY_BRACE,
|
||||
VARIABLE_SYNTAX,
|
||||
FORMAT_SPECIFIER,
|
||||
UNKNOWN_VARIABLE,
|
||||
};
|
||||
|
||||
struct Error {
|
||||
ErrorType type;
|
||||
blender::IndexRange byte_range;
|
||||
};
|
||||
|
||||
bool operator==(const Error &left, const Error &right);
|
||||
|
||||
} // namespace blender::bke::path_templates
|
||||
|
||||
/**
|
||||
* Build a template variable map based on available information.
|
||||
*
|
||||
* All parameters are allowed to be null, in which case the variables derived
|
||||
* from those parameters will simply not be included.
|
||||
*
|
||||
* This is typically used to create the variables passed to
|
||||
* `BKE_path_apply_template()`.
|
||||
*
|
||||
* \param blend_file_path: full path to the blend file, including the file name.
|
||||
* Typically you should fetch this with `ID_BLEND_PATH()`, but there are
|
||||
* exceptions. The key thing is that this should be the path to the *relevant*
|
||||
* blend file for the context that the variables are going to be used in. For
|
||||
* example, if the context is a linked ID then this path should (very likely) be
|
||||
* the path to that ID's library blend file, not the currently opened one.
|
||||
*
|
||||
* \param render_data: used for output resolution and fps. Note for the future:
|
||||
* when we add a "current frame number" variable it should *not* come from this
|
||||
* parameter, but be passed separately. This is because the callers of this
|
||||
* function sometimes have the current frame defined separately from the
|
||||
* available RenderData (see e.g. `do_makepicstring()`).
|
||||
*
|
||||
* \see BKE_path_apply_template()
|
||||
*
|
||||
* \see BLI_path_abs()
|
||||
*/
|
||||
blender::bke::path_templates::VariableMap BKE_build_template_variables(
|
||||
const char *blend_file_path, const RenderData *render_data);
|
||||
|
||||
/**
|
||||
* Validate the template syntax in the given path.
|
||||
*
|
||||
* This does *not* validate whether any variables referenced in the path exist
|
||||
* or not, nor whether the format specification in a variable expression is
|
||||
* appropriate for its type. This only validates that the template syntax itself
|
||||
* is valid.
|
||||
*
|
||||
* \return An empty vector if valid, or a vector of the parse errors if invalid.
|
||||
*/
|
||||
blender::Vector<blender::bke::path_templates::Error> BKE_validate_template_syntax(
|
||||
blender::StringRef path);
|
||||
|
||||
/**
|
||||
* Perform variable substitution and escaping on the given path.
|
||||
*
|
||||
* This mutates the path in-place. `path` must be a null-terminated string.
|
||||
*
|
||||
* The syntax for template expressions is `{variable_name}` or
|
||||
* {variable_name:format_spec}`. The format specification syntax currently only
|
||||
* applies to numerical values (integer or float), and uses hash symbols (#) to
|
||||
* indicate the number of digits to print the number with. It can be in any of
|
||||
* the following forms:
|
||||
*
|
||||
* - `####`: format as an integer with at least 4 digits, padding with zeros as
|
||||
* needed.
|
||||
* - `.###`: format as a float with precisely 3 fractional digits.
|
||||
* - `##.###`: format as a float with at least 2 integer-part digits (padded
|
||||
* with zeros as necessary) and precisely 3 fractional-part digits.
|
||||
*
|
||||
* This function also processes a simple escape sequence for writing literal "{"
|
||||
* and "}": like Python format strings, double braces "{{" and "}}" are treated
|
||||
* as escape sequences for "{" and "}", and are substituted appropriately. Note
|
||||
* that this substitution only happens *outside* of the variable syntax, and
|
||||
* therefore cannot e.g. be used inside variable names.
|
||||
*
|
||||
* If any errors are encountered, the path is left unaltered and a list of all
|
||||
* errors encountered is returned. Errors include:
|
||||
*
|
||||
* - Variable expression syntax errors.
|
||||
* - Unescaped curly braces.
|
||||
* - Referenced variables that cannot be found.
|
||||
* - Format specifications that don't apply to the type of variable they're
|
||||
* paired with.
|
||||
*
|
||||
* \param path_max_length The maximum length that template expansion is allowed
|
||||
* to make the template-expanded path (in bytes), including the null terminator.
|
||||
* In general, this should be the size of the underlying allocation of `path`.
|
||||
*
|
||||
* \return On success, an empty vector. If there are errors, a vector of all
|
||||
* errors encountered.
|
||||
*/
|
||||
blender::Vector<blender::bke::path_templates::Error> BKE_path_apply_template(
|
||||
char *path,
|
||||
int path_max_length,
|
||||
const blender::bke::path_templates::VariableMap &template_variables);
|
||||
/**
|
||||
* Produces a human-readable error message for the given template error.
|
||||
*/
|
||||
std::string BKE_path_template_error_to_string(const blender::bke::path_templates::Error &error,
|
||||
blender::StringRef path);
|
||||
|
||||
/**
|
||||
* Logs a report for the given template errors, with human-readable error
|
||||
* messages.
|
||||
*/
|
||||
void BKE_report_path_template_errors(ReportList *reports,
|
||||
eReportType report_type,
|
||||
blender::StringRef path,
|
||||
blender::Span<blender::bke::path_templates::Error> errors);
|
||||
@@ -252,6 +252,7 @@ set(SRC
|
||||
intern/particle_child.cc
|
||||
intern/particle_distribute.cc
|
||||
intern/particle_system.cc
|
||||
intern/path_templates.cc
|
||||
intern/pbvh.cc
|
||||
intern/pbvh_bmesh.cc
|
||||
intern/pbvh_pixels.cc
|
||||
@@ -484,6 +485,7 @@ set(SRC
|
||||
BKE_paint_bvh.hh
|
||||
BKE_paint_bvh_pixels.hh
|
||||
BKE_particle.h
|
||||
BKE_path_templates.hh
|
||||
BKE_pointcache.h
|
||||
BKE_pointcloud.hh
|
||||
BKE_pose_backup.h
|
||||
@@ -835,6 +837,7 @@ if(WITH_GTESTS)
|
||||
intern/lib_remap_test.cc
|
||||
intern/main_test.cc
|
||||
intern/nla_test.cc
|
||||
intern/path_templates_test.cc
|
||||
intern/subdiv_ccg_test.cc
|
||||
intern/tracking_test.cc
|
||||
intern/volume_test.cc
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
#include "BKE_colortools.hh"
|
||||
#include "BKE_image_format.hh"
|
||||
#include "BKE_path_templates.hh"
|
||||
|
||||
namespace path_templates = blender::bke::path_templates;
|
||||
|
||||
/* Init/Copy/Free */
|
||||
|
||||
@@ -597,20 +600,31 @@ int BKE_image_path_ext_from_imtype_ensure(char *filepath,
|
||||
return do_ensure_image_extension(filepath, filepath_maxncpy, imtype, nullptr);
|
||||
}
|
||||
|
||||
static void do_makepicstring(char filepath[FILE_MAX],
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
int frame,
|
||||
const char imtype,
|
||||
const ImageFormatData *im_format,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
static blender::Vector<path_templates::Error> do_makepicstring(
|
||||
char filepath[FILE_MAX],
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
const path_templates::VariableMap *template_variables,
|
||||
int frame,
|
||||
const char imtype,
|
||||
const ImageFormatData *im_format,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
{
|
||||
if (filepath == nullptr) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
BLI_strncpy(filepath, base, FILE_MAX - 10); /* weak assumption */
|
||||
|
||||
if (template_variables) {
|
||||
const blender::Vector<path_templates::Error> variable_errors = BKE_path_apply_template(
|
||||
filepath, FILE_MAX, *template_variables);
|
||||
if (!variable_errors.is_empty()) {
|
||||
return variable_errors;
|
||||
}
|
||||
}
|
||||
|
||||
BLI_path_abs(filepath, relbase);
|
||||
|
||||
if (use_frames) {
|
||||
@@ -624,31 +638,54 @@ static void do_makepicstring(char filepath[FILE_MAX],
|
||||
if (use_ext) {
|
||||
do_ensure_image_extension(filepath, FILE_MAX, imtype, im_format);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void BKE_image_path_from_imformat(char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
int frame,
|
||||
const ImageFormatData *im_format,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
blender::Vector<path_templates::Error> BKE_image_path_from_imformat(
|
||||
char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
const path_templates::VariableMap *template_variables,
|
||||
int frame,
|
||||
const ImageFormatData *im_format,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
{
|
||||
do_makepicstring(
|
||||
filepath, base, relbase, frame, im_format->imtype, im_format, use_ext, use_frames, suffix);
|
||||
return do_makepicstring(filepath,
|
||||
base,
|
||||
relbase,
|
||||
template_variables,
|
||||
frame,
|
||||
im_format->imtype,
|
||||
im_format,
|
||||
use_ext,
|
||||
use_frames,
|
||||
suffix);
|
||||
}
|
||||
|
||||
void BKE_image_path_from_imtype(char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
int frame,
|
||||
const char imtype,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
blender::Vector<path_templates::Error> BKE_image_path_from_imtype(
|
||||
char *filepath,
|
||||
const char *base,
|
||||
const char *relbase,
|
||||
const path_templates::VariableMap *template_variables,
|
||||
int frame,
|
||||
const char imtype,
|
||||
const bool use_ext,
|
||||
const bool use_frames,
|
||||
const char *suffix)
|
||||
{
|
||||
do_makepicstring(filepath, base, relbase, frame, imtype, nullptr, use_ext, use_frames, suffix);
|
||||
return do_makepicstring(filepath,
|
||||
base,
|
||||
relbase,
|
||||
template_variables,
|
||||
frame,
|
||||
imtype,
|
||||
nullptr,
|
||||
use_ext,
|
||||
use_frames,
|
||||
suffix);
|
||||
}
|
||||
|
||||
/* ImBuf Conversion */
|
||||
|
||||
@@ -1141,8 +1141,11 @@ static void cache_filepath(
|
||||
|
||||
BLI_path_join(cachepath, sizeof(cachepath), dirname, filename);
|
||||
|
||||
BKE_image_path_from_imtype(
|
||||
filepath, cachepath, relbase, frame, R_IMF_IMTYPE_OPENEXR, true, true, "");
|
||||
const blender::Vector<blender::bke::path_templates::Error> errors = BKE_image_path_from_imtype(
|
||||
filepath, cachepath, relbase, nullptr, frame, R_IMF_IMTYPE_OPENEXR, true, true, "");
|
||||
BLI_assert_msg(errors.is_empty(),
|
||||
"Path parsing errors should only occur when a variable map is provided.");
|
||||
UNUSED_VARS_NDEBUG(errors);
|
||||
}
|
||||
|
||||
/* silly functions but useful to inline when the args do a lot of indirections */
|
||||
|
||||
766
source/blender/blenkernel/intern/path_templates.cc
Normal file
766
source/blender/blenkernel/intern/path_templates.cc
Normal file
@@ -0,0 +1,766 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "BKE_path_templates.hh"
|
||||
#include "BKE_scene.hh"
|
||||
|
||||
namespace blender::bke::path_templates {
|
||||
|
||||
bool VariableMap::contains(blender::StringRef name) const
|
||||
{
|
||||
if (this->strings_.contains(name)) {
|
||||
return true;
|
||||
}
|
||||
if (this->integers_.contains(name)) {
|
||||
return true;
|
||||
}
|
||||
if (this->floats_.contains(name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VariableMap::remove(blender::StringRef name)
|
||||
{
|
||||
if (this->strings_.remove(name)) {
|
||||
return true;
|
||||
}
|
||||
if (this->integers_.remove(name)) {
|
||||
return true;
|
||||
}
|
||||
if (this->floats_.remove(name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VariableMap::add_string(blender::StringRef name, blender::StringRef value)
|
||||
{
|
||||
if (this->contains(name)) {
|
||||
return false;
|
||||
}
|
||||
this->strings_.add_new(name, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VariableMap::add_integer(blender::StringRef name, const int64_t value)
|
||||
{
|
||||
if (this->contains(name)) {
|
||||
return false;
|
||||
}
|
||||
this->integers_.add_new(name, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VariableMap::add_float(blender::StringRef name, const double value)
|
||||
{
|
||||
if (this->contains(name)) {
|
||||
return false;
|
||||
}
|
||||
this->floats_.add_new(name, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<blender::StringRefNull> VariableMap::get_string(blender::StringRef name) const
|
||||
{
|
||||
const std::string *value = this->strings_.lookup_ptr(name);
|
||||
if (value == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return blender::StringRefNull(*value);
|
||||
}
|
||||
|
||||
std::optional<int64_t> VariableMap::get_integer(blender::StringRef name) const
|
||||
{
|
||||
const int64_t *value = this->integers_.lookup_ptr(name);
|
||||
if (value == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
std::optional<double> VariableMap::get_float(blender::StringRef name) const
|
||||
{
|
||||
const double *value = this->floats_.lookup_ptr(name);
|
||||
if (value == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
bool operator==(const Error &left, const Error &right)
|
||||
{
|
||||
return left.type == right.type && left.byte_range == right.byte_range;
|
||||
}
|
||||
|
||||
} // namespace blender::bke::path_templates
|
||||
|
||||
using namespace blender::bke::path_templates;
|
||||
|
||||
VariableMap BKE_build_template_variables(const char *blend_file_path,
|
||||
const RenderData *render_data)
|
||||
{
|
||||
VariableMap variables;
|
||||
|
||||
/* Blend file name. */
|
||||
if (blend_file_path) {
|
||||
const char *file_name = BLI_path_basename(blend_file_path);
|
||||
const char *file_name_end = BLI_path_extension_or_end(file_name);
|
||||
if (file_name[0] == '\0') {
|
||||
/* If the file has never been saved (indicated by an empty file name),
|
||||
* default to "Unsaved". */
|
||||
variables.add_string("blend_name", blender::StringRef(DATA_("Unsaved")));
|
||||
}
|
||||
else if (file_name_end == file_name) {
|
||||
/* When the filename has no extension, but starts with a period. */
|
||||
variables.add_string("blend_name", blender::StringRef(file_name));
|
||||
}
|
||||
else {
|
||||
/* Normal case. */
|
||||
variables.add_string("blend_name", blender::StringRef(file_name, file_name_end));
|
||||
}
|
||||
}
|
||||
|
||||
/* Render resolution and fps. */
|
||||
if (render_data) {
|
||||
int res_x, res_y;
|
||||
BKE_render_resolution(render_data, false, &res_x, &res_y);
|
||||
variables.add_integer("resolution_x", res_x);
|
||||
variables.add_integer("resolution_y", res_y);
|
||||
|
||||
/* FPS eval code copied from `BKE_cachefile_filepath_get()`.
|
||||
*
|
||||
* TODO: should probably use one function for this everywhere to ensure that
|
||||
* fps is computed consistently, but at the time of writing no such function
|
||||
* seems to exist. Every place in the code base just has its own bespoke
|
||||
* code, using different precision, etc. */
|
||||
const double fps = double(render_data->frs_sec) / double(render_data->frs_sec_base);
|
||||
variables.add_float("fps", fps);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
#define FORMAT_BUFFER_SIZE 128
|
||||
|
||||
namespace {
|
||||
|
||||
enum class FormatSpecifierType {
|
||||
/* No format specifier given. Use default formatting. */
|
||||
NONE = 0,
|
||||
|
||||
/* The format specifier was a string of just "#" characters. E.g. "####". */
|
||||
INTEGER,
|
||||
|
||||
/* The format specifier was a string of "#" characters with a single ".". E.g.
|
||||
* "###.##". */
|
||||
FLOAT,
|
||||
|
||||
/* The format specifier was invalid due to incorrect syntax. */
|
||||
SYNTAX_ERROR,
|
||||
};
|
||||
|
||||
/**
|
||||
* Specifies how a variable should be formatted into a string, or indicates a
|
||||
* parse error.
|
||||
*/
|
||||
struct FormatSpecifier {
|
||||
FormatSpecifierType type = FormatSpecifierType::NONE;
|
||||
|
||||
/* For INTEGER and FLOAT formatting types, the number of digits indicated on
|
||||
* either side of the decimal point. */
|
||||
std::optional<uint8_t> integer_digit_count;
|
||||
std::optional<uint8_t> fractional_digit_count;
|
||||
};
|
||||
|
||||
enum class TokenType {
|
||||
/* Either "{variable_name}" or "{variable_name:format_spec}". */
|
||||
VARIABLE_EXPRESSION,
|
||||
|
||||
/* "{{", which is an escaped "{". */
|
||||
LEFT_CURLY_BRACE,
|
||||
|
||||
/* "}}", which is an escaped "}". */
|
||||
RIGHT_CURLY_BRACE,
|
||||
|
||||
/* Encountered a syntax error while trying to parse a variable expression. */
|
||||
VARIABLE_SYNTAX_ERROR,
|
||||
|
||||
/* Encountered an unescaped curly brace in an invalid position. */
|
||||
UNESCAPED_CURLY_BRACE_ERROR,
|
||||
};
|
||||
|
||||
/**
|
||||
* A token that was parsed and should be substituted in the string, or an error.
|
||||
*/
|
||||
struct Token {
|
||||
TokenType type = TokenType::VARIABLE_EXPRESSION;
|
||||
|
||||
/* Byte index range (exclusive on the right) of the token or syntax error in
|
||||
* the path string. */
|
||||
blender::IndexRange byte_range;
|
||||
|
||||
/* Reference to the the variable name as written in the template string. Note
|
||||
* that this points into the template string, and does not own the value.
|
||||
*
|
||||
* Only relevant when `type == VARIABLE_EXPRESSION`. */
|
||||
blender::StringRef variable_name;
|
||||
|
||||
/* Indicates how the variable's value should be formatted into a string. This
|
||||
* is derived from the format specification (e.g. the "###" in "{blah:###}").
|
||||
*
|
||||
* Only relevant when `type == VARIABLE_EXPRESSION`. */
|
||||
FormatSpecifier format;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* Format an integer into a string, according to `format`.
|
||||
*
|
||||
* Note: if `format` is not valid for integers, the resulting string will be
|
||||
* empty.
|
||||
*
|
||||
* \return length of the produced string. Zero indicates an error.
|
||||
*/
|
||||
static int format_int_to_string(const FormatSpecifier &format,
|
||||
const int64_t integer_value,
|
||||
char r_output_string[FORMAT_BUFFER_SIZE])
|
||||
{
|
||||
BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
|
||||
|
||||
r_output_string[0] = '\0';
|
||||
int output_length = 0;
|
||||
|
||||
switch (format.type) {
|
||||
case FormatSpecifierType::NONE: {
|
||||
output_length = sprintf(r_output_string, "%ld", integer_value);
|
||||
break;
|
||||
}
|
||||
|
||||
case FormatSpecifierType::INTEGER: {
|
||||
BLI_assert(format.integer_digit_count.has_value());
|
||||
BLI_assert(*format.integer_digit_count > 0);
|
||||
output_length = sprintf(
|
||||
r_output_string, "%0*ld", *format.integer_digit_count, integer_value);
|
||||
break;
|
||||
}
|
||||
|
||||
case FormatSpecifierType::FLOAT: {
|
||||
/* Formatting an integer as a float: we do *not* defer to the float
|
||||
* formatter for this because we could lose precision with very large
|
||||
* numbers. Instead we simply print the integer, and then append ".000..."
|
||||
* to it. */
|
||||
BLI_assert(format.fractional_digit_count.has_value());
|
||||
BLI_assert(*format.fractional_digit_count > 0);
|
||||
|
||||
if (format.integer_digit_count.has_value()) {
|
||||
BLI_assert(*format.integer_digit_count > 0);
|
||||
output_length = sprintf(
|
||||
r_output_string, "%0*ld", *format.integer_digit_count, integer_value);
|
||||
}
|
||||
else {
|
||||
output_length = sprintf(r_output_string, "%ld", integer_value);
|
||||
}
|
||||
|
||||
r_output_string[output_length] = '.';
|
||||
output_length++;
|
||||
|
||||
for (int i = 0; i < *format.fractional_digit_count; i++) {
|
||||
r_output_string[output_length] = '0';
|
||||
output_length++;
|
||||
}
|
||||
|
||||
r_output_string[output_length] = '\0';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case FormatSpecifierType::SYNTAX_ERROR: {
|
||||
BLI_assert_msg(
|
||||
false,
|
||||
"Format specifiers with invalid syntax should have been rejected before getting here.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return output_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a floating point number into a string, according to `format`.
|
||||
*
|
||||
* Note: if `format` is not valid for floating point numbers, the resulting
|
||||
* string will be empty.
|
||||
*
|
||||
* \return length of the produced string. Zero indicates an error
|
||||
*/
|
||||
static int format_float_to_string(const FormatSpecifier &format,
|
||||
const double float_value,
|
||||
char r_output_string[FORMAT_BUFFER_SIZE])
|
||||
{
|
||||
BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
|
||||
|
||||
r_output_string[0] = '\0';
|
||||
int output_length = 0;
|
||||
|
||||
switch (format.type) {
|
||||
case FormatSpecifierType::NONE: {
|
||||
/* When no format specification is given, we attempt to approximate
|
||||
* Python's behavior in the same situation. We can't exactly match via
|
||||
* `sprintf()`, but we can get pretty close. The only major thing we can't
|
||||
* replicate via `sprintf()` is that in Python whole numbers are printed
|
||||
* with a trailing ".0". So we handle that bit manually. */
|
||||
output_length = sprintf(r_output_string, "%.16g", float_value);
|
||||
|
||||
/* If the string consists only of digits and a possible negative sign, then
|
||||
* we append a ".0" to match Python. */
|
||||
if (blender::StringRef(r_output_string).find_first_not_of("-0123456789") ==
|
||||
std::string::npos)
|
||||
{
|
||||
r_output_string[output_length] = '.';
|
||||
r_output_string[output_length + 1] = '0';
|
||||
r_output_string[output_length + 2] = '\0';
|
||||
output_length += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case FormatSpecifierType::INTEGER: {
|
||||
/* Defer to the integer formatter with a rounded value. */
|
||||
return format_int_to_string(format, std::round(float_value), r_output_string);
|
||||
}
|
||||
|
||||
case FormatSpecifierType::FLOAT: {
|
||||
BLI_assert(format.fractional_digit_count.has_value());
|
||||
BLI_assert(*format.fractional_digit_count > 0);
|
||||
|
||||
if (format.integer_digit_count.has_value()) {
|
||||
/* Both integer and fractional component lengths are specified. */
|
||||
BLI_assert(*format.integer_digit_count > 0);
|
||||
output_length = sprintf(r_output_string,
|
||||
"%0*.*f",
|
||||
*format.integer_digit_count + *format.fractional_digit_count + 1,
|
||||
*format.fractional_digit_count,
|
||||
float_value);
|
||||
}
|
||||
else {
|
||||
/* Only fractional component length is specified. */
|
||||
output_length = sprintf(
|
||||
r_output_string, "%.*f", *format.fractional_digit_count, float_value);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case FormatSpecifierType::SYNTAX_ERROR: {
|
||||
BLI_assert_msg(
|
||||
false,
|
||||
"Format specifiers with invalid syntax should have been rejected before getting here.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return output_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the "format specifier" part of a variable expression.
|
||||
*
|
||||
* The format specifier is e.g. the "##.###" in "{name:##.###}". The specifier
|
||||
* string should be passed alone (just the "##.###"), without the rest of the
|
||||
* variable expression.
|
||||
*/
|
||||
static FormatSpecifier parse_format_specifier(blender::StringRef format_specifier)
|
||||
{
|
||||
FormatSpecifier format = {};
|
||||
|
||||
/* A ":" was used, but no format specifier was given, which is invalid. */
|
||||
if (format_specifier.is_empty()) {
|
||||
format.type = FormatSpecifierType::SYNTAX_ERROR;
|
||||
return format;
|
||||
}
|
||||
|
||||
/* If it's all digit specifiers, then format as an integer. */
|
||||
if (format_specifier.find_first_not_of("#") == std::string::npos) {
|
||||
format.integer_digit_count = format_specifier.size();
|
||||
|
||||
format.type = FormatSpecifierType::INTEGER;
|
||||
return format;
|
||||
}
|
||||
|
||||
/* If it's digit specifiers and a dot, format as a float. */
|
||||
const int64_t dot_index = format_specifier.find_first_of('.');
|
||||
const int64_t dot_index_last = format_specifier.find_last_of('.');
|
||||
const bool found_dot = dot_index != std::string::npos;
|
||||
const bool only_one_dot = dot_index == dot_index_last;
|
||||
if (format_specifier.find_first_not_of(".#") == std::string::npos && found_dot && only_one_dot) {
|
||||
blender::StringRef left = format_specifier.substr(0, dot_index);
|
||||
blender::StringRef right = format_specifier.substr(dot_index + 1);
|
||||
|
||||
/* We currently require that the fractional digits are specified, so bail if
|
||||
* they aren't. */
|
||||
if (right.is_empty()) {
|
||||
format.type = FormatSpecifierType::SYNTAX_ERROR;
|
||||
return format;
|
||||
}
|
||||
|
||||
if (!left.is_empty()) {
|
||||
format.integer_digit_count = left.size();
|
||||
}
|
||||
|
||||
format.fractional_digit_count = right.size();
|
||||
|
||||
format.type = FormatSpecifierType::FLOAT;
|
||||
return format;
|
||||
}
|
||||
|
||||
format.type = FormatSpecifierType::SYNTAX_ERROR;
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and parse the next valid token in `path` starting from index
|
||||
* `from_char`.
|
||||
*
|
||||
* \param path The path string to parse.
|
||||
*
|
||||
* \param from_char The char index to start from.
|
||||
*
|
||||
* \return The parsed token information, or nullopt if no token is found in
|
||||
* `path`.
|
||||
*/
|
||||
static std::optional<Token> next_token(blender::StringRef path, const int from_char)
|
||||
{
|
||||
Token token;
|
||||
|
||||
/* We use the magic number -1 here to indicate that a component hasn't been
|
||||
* found yet. When a component is found, the respective token here is set
|
||||
* to the byte offset it was found at. */
|
||||
int start = -1; /* "{" */
|
||||
int format_specifier_split = -1; /* ":" */
|
||||
int end = -1; /* "}" */
|
||||
|
||||
for (int byte_index = from_char; byte_index < path.size(); byte_index++) {
|
||||
/* Check for escaped "{". */
|
||||
if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '{' &&
|
||||
path[byte_index + 1] == '{')
|
||||
{
|
||||
Token token;
|
||||
token.type = TokenType::LEFT_CURLY_BRACE;
|
||||
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
|
||||
return token;
|
||||
}
|
||||
|
||||
/* Check for escaped "}".
|
||||
*
|
||||
* Note that we only do this check when not already inside a variable
|
||||
* expression, since it could be a valid closing "}" followed by additional
|
||||
* escaped closing braces. */
|
||||
if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '}' &&
|
||||
path[byte_index + 1] == '}')
|
||||
{
|
||||
token.type = TokenType::RIGHT_CURLY_BRACE;
|
||||
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
|
||||
return token;
|
||||
}
|
||||
|
||||
/* Check for unescaped "}", which outside of a variable expression is
|
||||
* illegal. */
|
||||
if (start == -1 && path[byte_index] == '}') {
|
||||
token.type = TokenType::UNESCAPED_CURLY_BRACE_ERROR;
|
||||
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 1);
|
||||
return token;
|
||||
}
|
||||
|
||||
/* Check if we've found a starting "{". */
|
||||
if (path[byte_index] == '{') {
|
||||
if (start != -1) {
|
||||
/* Already inside a variable expression. */
|
||||
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
|
||||
token.byte_range = blender::IndexRange::from_begin_end(start, byte_index);
|
||||
return token;
|
||||
}
|
||||
start = byte_index;
|
||||
format_specifier_split = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If we haven't found a start, we shouldn't try to parse the other bits
|
||||
* yet. */
|
||||
if (start == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if we've found a format splitter. */
|
||||
if (path[byte_index] == ':') {
|
||||
if (format_specifier_split == -1) {
|
||||
/* Only set if it's the first ":" we've encountered in the variable
|
||||
* expression. Subsequent ones will be handled in the format specifier
|
||||
* parsing. */
|
||||
format_specifier_split = byte_index;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if we've found the closing "}". */
|
||||
if (path[byte_index] == '}') {
|
||||
end = byte_index + 1; /* Exclusive end. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* No variable expression found. */
|
||||
if (start == -1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/* Unclosed variable expression. Syntax error. */
|
||||
if (end == -1) {
|
||||
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
|
||||
token.byte_range = blender::IndexRange::from_begin_end(start, path.size());
|
||||
return token;
|
||||
}
|
||||
|
||||
/* Parse the variable expression we found. */
|
||||
token.byte_range = blender::IndexRange::from_begin_end(start, end);
|
||||
if (format_specifier_split == -1) {
|
||||
/* No format specifier. */
|
||||
token.variable_name = path.substr(start + 1, (end - 1) - (start + 1));
|
||||
}
|
||||
else {
|
||||
/* Found format specifier. */
|
||||
token.variable_name = path.substr(start + 1, format_specifier_split - (start + 1));
|
||||
token.format = parse_format_specifier(
|
||||
path.substr(format_specifier_split + 1, (end - 1) - (format_specifier_split + 1)));
|
||||
if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
|
||||
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/* Parse the given template and return the list of tokens found, in the same
|
||||
* order as they appear in the template. */
|
||||
static blender::Vector<Token> parse_template(blender::StringRef path)
|
||||
{
|
||||
blender::Vector<Token> tokens;
|
||||
|
||||
for (int bytes_read = 0; bytes_read < path.size();) {
|
||||
const std::optional<Token> token = next_token(path, bytes_read);
|
||||
|
||||
if (!token.has_value()) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytes_read = token->byte_range.one_after_last();
|
||||
tokens.append(*token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/* Convert a token to its corresponding syntax error. If the token doesn't have
|
||||
* an error, returns nullopt. */
|
||||
static std::optional<Error> token_to_syntax_error(const Token &token)
|
||||
{
|
||||
switch (token.type) {
|
||||
case TokenType::VARIABLE_SYNTAX_ERROR: {
|
||||
if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
|
||||
return {{ErrorType::FORMAT_SPECIFIER, token.byte_range}};
|
||||
}
|
||||
else {
|
||||
return {{ErrorType::VARIABLE_SYNTAX, token.byte_range}};
|
||||
}
|
||||
}
|
||||
|
||||
case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
|
||||
return {{ErrorType::UNESCAPED_CURLY_BRACE, token.byte_range}};
|
||||
}
|
||||
|
||||
/* Non-errors. */
|
||||
case TokenType::VARIABLE_EXPRESSION:
|
||||
case TokenType::LEFT_CURLY_BRACE:
|
||||
case TokenType::RIGHT_CURLY_BRACE:
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
BLI_assert_msg(false, "Unhandled token type.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
blender::Vector<Error> BKE_validate_template_syntax(blender::StringRef path)
|
||||
{
|
||||
const blender::Vector<Token> tokens = parse_template(path);
|
||||
|
||||
blender::Vector<Error> errors;
|
||||
for (const Token &token : tokens) {
|
||||
if (std::optional<Error> error = token_to_syntax_error(token)) {
|
||||
errors.append(*error);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
blender::Vector<Error> BKE_path_apply_template(char *path,
|
||||
int path_max_length,
|
||||
const VariableMap &template_variables)
|
||||
{
|
||||
const blender::Vector<Token> tokens = parse_template(path);
|
||||
|
||||
if (tokens.is_empty()) {
|
||||
/* No tokens found, so nothing to do. */
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Accumulates errors as we process the tokens. */
|
||||
blender::Vector<Error> errors;
|
||||
|
||||
/* We work on a copy of the path, for two reasons:
|
||||
*
|
||||
* 1. So that if there are errors we can leave the original unmodified.
|
||||
* 2. So that the contents of the StringRefs in the Token structs don't change
|
||||
* out from under us while we're generating the modified path.*/
|
||||
blender::Vector<char> path_buffer(path_max_length);
|
||||
char *path_modified = path_buffer.data();
|
||||
strcpy(path_modified, path);
|
||||
|
||||
/* Tracks the change in string length due to the modifications as we go. We
|
||||
* need this to properly map the token byte ranges to the being-modified
|
||||
* string. */
|
||||
int length_diff = 0;
|
||||
|
||||
for (const Token &token : tokens) {
|
||||
/* Syntax errors. */
|
||||
if (std::optional<Error> error = token_to_syntax_error(token)) {
|
||||
errors.append(*error);
|
||||
continue;
|
||||
}
|
||||
|
||||
char replacement_string[FORMAT_BUFFER_SIZE];
|
||||
|
||||
switch (token.type) {
|
||||
/* Syntax errors should have been handled above. */
|
||||
case TokenType::VARIABLE_SYNTAX_ERROR:
|
||||
case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
|
||||
BLI_assert_msg(false, "Unhandled syntax error.");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Curly brace escapes. */
|
||||
case TokenType::LEFT_CURLY_BRACE: {
|
||||
strcpy(replacement_string, "{");
|
||||
break;
|
||||
}
|
||||
case TokenType::RIGHT_CURLY_BRACE: {
|
||||
strcpy(replacement_string, "}");
|
||||
break;
|
||||
}
|
||||
|
||||
/* Expand variable expression into the variable's value. */
|
||||
case TokenType::VARIABLE_EXPRESSION: {
|
||||
if (std::optional<blender::StringRefNull> string_value = template_variables.get_string(
|
||||
token.variable_name))
|
||||
{
|
||||
/* String variable found, but we only process it if there's no format
|
||||
* specifier: string variables do not support format specifiers. */
|
||||
if (token.format.type != FormatSpecifierType::NONE) {
|
||||
/* String variables don't take format specifiers: error. */
|
||||
errors.append({ErrorType::FORMAT_SPECIFIER, token.byte_range});
|
||||
continue;
|
||||
}
|
||||
strcpy(replacement_string, string_value->c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
if (std::optional<int64_t> integer_value = template_variables.get_integer(
|
||||
token.variable_name))
|
||||
{
|
||||
/* Integer variable found. */
|
||||
format_int_to_string(token.format, *integer_value, replacement_string);
|
||||
break;
|
||||
}
|
||||
|
||||
if (std::optional<double> float_value = template_variables.get_float(token.variable_name))
|
||||
{
|
||||
/* Float variable found. */
|
||||
format_float_to_string(token.format, *float_value, replacement_string);
|
||||
break;
|
||||
}
|
||||
|
||||
/* No matching variable found: error. */
|
||||
errors.append({ErrorType::UNKNOWN_VARIABLE, token.byte_range});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* We're off the end of the available space. */
|
||||
if (token.byte_range.start() + length_diff >= path_max_length) {
|
||||
break;
|
||||
}
|
||||
|
||||
BLI_string_replace_range(path_modified,
|
||||
path_max_length,
|
||||
token.byte_range.start() + length_diff,
|
||||
token.byte_range.one_after_last() + length_diff,
|
||||
replacement_string);
|
||||
|
||||
length_diff -= token.byte_range.size();
|
||||
length_diff += strlen(replacement_string);
|
||||
}
|
||||
|
||||
if (errors.is_empty()) {
|
||||
/* No errors, so copy the modified path back to the original. */
|
||||
strcpy(path, path_modified);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
std::string BKE_path_template_error_to_string(const Error &error, blender::StringRef path)
|
||||
{
|
||||
blender::StringRef subpath = path.substr(error.byte_range.start(), error.byte_range.size());
|
||||
|
||||
switch (error.type) {
|
||||
case ErrorType::UNESCAPED_CURLY_BRACE: {
|
||||
return std::string("Unescaped curly brace '") + subpath + "'.";
|
||||
}
|
||||
|
||||
case ErrorType::VARIABLE_SYNTAX: {
|
||||
return std::string("Invalid or incomplete template expression '") + subpath + "'.";
|
||||
}
|
||||
|
||||
case ErrorType::FORMAT_SPECIFIER: {
|
||||
return std::string("Invalid format specifier in template expression '") + subpath + "'.";
|
||||
}
|
||||
|
||||
case ErrorType::UNKNOWN_VARIABLE: {
|
||||
return std::string("Unknown variable referenced in template expression '") + subpath + "'.";
|
||||
}
|
||||
}
|
||||
|
||||
BLI_assert_msg(false, "Unhandled error type.");
|
||||
return "Unknown error.";
|
||||
}
|
||||
|
||||
void BKE_report_path_template_errors(ReportList *reports,
|
||||
const eReportType report_type,
|
||||
blender::StringRef path,
|
||||
blender::Span<Error> errors)
|
||||
{
|
||||
BLI_assert(!errors.is_empty());
|
||||
|
||||
std::string error_message = "Parse errors in path '" + path + "':";
|
||||
for (const Error &error : errors) {
|
||||
error_message += "\n- " + BKE_path_template_error_to_string(error, path);
|
||||
}
|
||||
|
||||
BKE_report(reports, report_type, error_message.c_str());
|
||||
}
|
||||
342
source/blender/blenkernel/intern/path_templates_test.cc
Normal file
342
source/blender/blenkernel/intern/path_templates_test.cc
Normal file
@@ -0,0 +1,342 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_path_templates.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::bke::tests {
|
||||
|
||||
using namespace blender::bke::path_templates;
|
||||
|
||||
[[maybe_unused]] static void debug_print_error(const Error &error)
|
||||
{
|
||||
const char *type;
|
||||
switch (error.type) {
|
||||
case ErrorType::UNESCAPED_CURLY_BRACE:
|
||||
type = "UNESCAPED_CURLY_BRACE";
|
||||
break;
|
||||
case ErrorType::VARIABLE_SYNTAX:
|
||||
type = "VARIABLE_SYNTAX";
|
||||
break;
|
||||
case ErrorType::FORMAT_SPECIFIER:
|
||||
type = "FORMAT_SPECIFIER";
|
||||
break;
|
||||
case ErrorType::UNKNOWN_VARIABLE:
|
||||
type = "UNKNOWN_VARIABLE";
|
||||
break;
|
||||
}
|
||||
printf("(%s, (%ld, %ld))", type, error.byte_range.start(), error.byte_range.size());
|
||||
}
|
||||
|
||||
[[maybe_unused]] static void debug_print_errors(Span<Error> errors)
|
||||
{
|
||||
printf("[");
|
||||
for (const Error &error : errors) {
|
||||
debug_print_error(error);
|
||||
printf(", ");
|
||||
}
|
||||
printf("]\n");
|
||||
}
|
||||
|
||||
TEST(path_templates, VariableMap)
|
||||
{
|
||||
VariableMap map;
|
||||
|
||||
/* With in empty variable map, these should all return false / fail. */
|
||||
EXPECT_FALSE(map.contains("hello"));
|
||||
EXPECT_FALSE(map.remove("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_string("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_integer("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_float("hello"));
|
||||
|
||||
/* Populate the map. */
|
||||
EXPECT_TRUE(map.add_string("hello", "What a wonderful world."));
|
||||
EXPECT_TRUE(map.add_integer("bye", 42));
|
||||
EXPECT_TRUE(map.add_float("what", 3.14159));
|
||||
|
||||
/* Attempting to add variables with those names again should fail, since they
|
||||
* already exist now. */
|
||||
EXPECT_FALSE(map.add_string("hello", "Sup."));
|
||||
EXPECT_FALSE(map.add_string("bye", "Sup."));
|
||||
EXPECT_FALSE(map.add_string("what", "Sup."));
|
||||
EXPECT_FALSE(map.add_integer("hello", 2));
|
||||
EXPECT_FALSE(map.add_integer("bye", 2));
|
||||
EXPECT_FALSE(map.add_integer("what", 2));
|
||||
EXPECT_FALSE(map.add_float("hello", 2.71828));
|
||||
EXPECT_FALSE(map.add_float("bye", 2.71828));
|
||||
EXPECT_FALSE(map.add_float("what", 2.71828));
|
||||
|
||||
/* Confirm that the right variables exist. */
|
||||
EXPECT_TRUE(map.contains("hello"));
|
||||
EXPECT_TRUE(map.contains("bye"));
|
||||
EXPECT_TRUE(map.contains("what"));
|
||||
EXPECT_FALSE(map.contains("not here"));
|
||||
|
||||
/* Fetch the variables we added. */
|
||||
EXPECT_EQ("What a wonderful world.", map.get_string("hello"));
|
||||
EXPECT_EQ(42, map.get_integer("bye"));
|
||||
EXPECT_EQ(3.14159, map.get_float("what"));
|
||||
|
||||
/* The same variables shouldn't exist for the other types, despite our attempt
|
||||
* to add them earlier. */
|
||||
EXPECT_EQ(std::nullopt, map.get_integer("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_float("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_string("bye"));
|
||||
EXPECT_EQ(std::nullopt, map.get_float("bye"));
|
||||
EXPECT_EQ(std::nullopt, map.get_string("what"));
|
||||
EXPECT_EQ(std::nullopt, map.get_integer("what"));
|
||||
|
||||
/* Remove the variables. */
|
||||
EXPECT_TRUE(map.remove("hello"));
|
||||
EXPECT_TRUE(map.remove("bye"));
|
||||
EXPECT_TRUE(map.remove("what"));
|
||||
|
||||
/* The variables shouldn't exist anymore. */
|
||||
EXPECT_FALSE(map.contains("hello"));
|
||||
EXPECT_FALSE(map.contains("bye"));
|
||||
EXPECT_FALSE(map.contains("what"));
|
||||
EXPECT_EQ(std::nullopt, map.get_string("hello"));
|
||||
EXPECT_EQ(std::nullopt, map.get_integer("bye"));
|
||||
EXPECT_EQ(std::nullopt, map.get_float("what"));
|
||||
EXPECT_FALSE(map.remove("hello"));
|
||||
EXPECT_FALSE(map.remove("bye"));
|
||||
EXPECT_FALSE(map.remove("what"));
|
||||
}
|
||||
|
||||
TEST(path_templates, path_apply_variables)
|
||||
{
|
||||
VariableMap variables;
|
||||
{
|
||||
variables.add_string("hi", "hello");
|
||||
variables.add_string("bye", "goodbye");
|
||||
variables.add_string("long", "This string is exactly 32 bytes.");
|
||||
variables.add_integer("the_answer", 42);
|
||||
variables.add_integer("prime", 7);
|
||||
variables.add_integer("i_negative", -7);
|
||||
variables.add_float("pi", 3.14159265358979323846);
|
||||
variables.add_float("e", 2.71828182845904523536);
|
||||
variables.add_float("ntsc", 30.0 / 1.001);
|
||||
variables.add_float("two", 2.0);
|
||||
variables.add_float("f_negative", -3.14159265358979323846);
|
||||
variables.add_float("huge", 200000000000000000000000000000000.0);
|
||||
variables.add_float("tiny", 0.000000000000000000000000000000002);
|
||||
}
|
||||
|
||||
/* Simple case, testing all variables. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"{hi}_{bye}_{the_answer}_{prime}_{i_negative}_{pi}_{e}_{ntsc}_{two}_{f_negative}_{huge}_{"
|
||||
"tiny}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path),
|
||||
"hello_goodbye_42_7_-7_3.141592653589793_2.718281828459045_29.97002997002997_2.0_-3."
|
||||
"141592653589793_2e+32_2e-33");
|
||||
}
|
||||
|
||||
/* Integer formatting. */
|
||||
{
|
||||
char path[FILE_MAX] = "{the_answer:#}_{the_answer:##}_{the_answer:####}_{i_negative:####}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path), "42_42_0042_-007");
|
||||
}
|
||||
|
||||
/* Integer formatting as float. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"{the_answer:.###}_{the_answer:#.##}_{the_answer:###.##}_{i_negative:###.####}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path), "42.000_42.00_042.00_-07.0000");
|
||||
}
|
||||
|
||||
/* Float formatting: specify fractional digits only. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"{pi:.####}_{e:.###}_{ntsc:.########}_{two:.##}_{f_negative:.##}_{huge:.##}_{tiny:.##}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path),
|
||||
"3.1416_2.718_29.97002997_2.00_-3.14_200000000000000010732324408786944.00_0.00");
|
||||
}
|
||||
|
||||
/* Float formatting: specify both integer and fractional digits. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"{pi:##.####}_{e:####.###}_{ntsc:#.########}_{two:###.##}_{f_negative:###.##}_{huge:###.##"
|
||||
"}_{tiny:###.##}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(
|
||||
blender::StringRef(path),
|
||||
"03.1416_0002.718_29.97002997_002.00_-03.14_200000000000000010732324408786944.00_000.00");
|
||||
}
|
||||
|
||||
/* Float formatting: format as integer. */
|
||||
{
|
||||
char path[FILE_MAX] = "{pi:##}_{e:####}_{ntsc:#}_{two:###}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path), "03_0003_30_002");
|
||||
}
|
||||
|
||||
/* Escaping. "{{" and "}}" are the escape codes for literal "{" and "}". */
|
||||
{
|
||||
char path[FILE_MAX] = "{hi}_{{hi}}_{{{bye}}}_{bye}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path), "hello_{hi}_{goodbye}_goodbye");
|
||||
}
|
||||
|
||||
/* Error: string variables do not support format specifiers. */
|
||||
{
|
||||
char path[FILE_MAX] = "{hi:##}_{bye:#}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 7)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(8, 7)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path), "{hi:##}_{bye:#}");
|
||||
}
|
||||
|
||||
/* Error: float formatting: specifying integer digits only (but still wanting
|
||||
* it printed as a float) is currently not supported. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 9)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(29, 10)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(40, 17)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(58, 11)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(70, 11)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path),
|
||||
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}");
|
||||
}
|
||||
|
||||
/* Error: missing variable. */
|
||||
{
|
||||
char path[FILE_MAX] = "{hi}_{missing}_{bye}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::UNKNOWN_VARIABLE, IndexRange(5, 9)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path), "{hi}_{missing}_{bye}");
|
||||
}
|
||||
|
||||
/* Error: incomplete variable expression. */
|
||||
{
|
||||
char path[FILE_MAX] = "foo{hi";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::VARIABLE_SYNTAX, IndexRange(3, 3)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path), "foo{hi");
|
||||
}
|
||||
|
||||
/* Error: invalid format specifiers. */
|
||||
{
|
||||
char path[FILE_MAX] = "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 13)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(33, 11)},
|
||||
{ErrorType::FORMAT_SPECIFIER, IndexRange(45, 12)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path),
|
||||
"{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}");
|
||||
}
|
||||
|
||||
/* Error: unclosed variable. */
|
||||
{
|
||||
char path[FILE_MAX] = "{hi_{hi}_{bye}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path), "{hi_{hi}_{bye}");
|
||||
}
|
||||
|
||||
/* Error: escaped braces inside variable. */
|
||||
{
|
||||
char path[FILE_MAX] = "{hi_{{hi}}_{bye}";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
const Vector<Error> expected_errors = {
|
||||
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
|
||||
};
|
||||
|
||||
EXPECT_EQ(errors, expected_errors);
|
||||
EXPECT_EQ(blender::StringRef(path), "{hi_{{hi}}_{bye}");
|
||||
}
|
||||
|
||||
/* Test what happens when the path would expand to a string that's longer than
|
||||
* `FILE_MAX`.
|
||||
*
|
||||
* We don't care so much about any kind of "correctness" here, we just want to
|
||||
* ensure that it still results in a valid null-terminated string that fits in
|
||||
* `FILE_MAX` bytes.
|
||||
*
|
||||
* NOTE: this test will have to be updated if `FILE_MAX` is ever changed. */
|
||||
{
|
||||
char path[FILE_MAX] =
|
||||
"___{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
|
||||
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
|
||||
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
|
||||
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
|
||||
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
|
||||
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
|
||||
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}";
|
||||
const char result[FILE_MAX] =
|
||||
"___This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
|
||||
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
|
||||
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
|
||||
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
|
||||
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
|
||||
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
|
||||
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
|
||||
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
|
||||
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
|
||||
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
|
||||
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
|
||||
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 by";
|
||||
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
|
||||
|
||||
EXPECT_TRUE(errors.is_empty());
|
||||
EXPECT_EQ(blender::StringRef(path), blender::StringRef(result));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::bke::tests
|
||||
@@ -3286,6 +3286,55 @@ static void do_version_alpha_over_node_options_to_inputs_animation(bNodeTree *no
|
||||
});
|
||||
}
|
||||
|
||||
/* Turns all instances of "{" and "}" in a string into "{{" and "}}", escaping
|
||||
* them for strings that are processed with templates so that they don't
|
||||
* erroneously get interepreted as template expressions. */
|
||||
static void version_escape_curly_braces(char string[], const int string_array_length)
|
||||
{
|
||||
int bytes_processed = 0;
|
||||
while (bytes_processed < string_array_length && string[bytes_processed] != '\0') {
|
||||
if (string[bytes_processed] == '{') {
|
||||
BLI_string_replace_range(
|
||||
string, string_array_length, bytes_processed, bytes_processed + 1, "{{");
|
||||
bytes_processed += 2;
|
||||
continue;
|
||||
}
|
||||
if (string[bytes_processed] == '}') {
|
||||
BLI_string_replace_range(
|
||||
string, string_array_length, bytes_processed, bytes_processed + 1, "}}");
|
||||
bytes_processed += 2;
|
||||
continue;
|
||||
}
|
||||
bytes_processed++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Escapes all instances of "{" and "}" in the paths in a compositor node tree's
|
||||
* File Output nodes.
|
||||
*
|
||||
* If the passed node tree is not a compositor node tree, does nothing. */
|
||||
static void version_escape_curly_braces_in_compositor_file_output_nodes(bNodeTree &nodetree)
|
||||
{
|
||||
if (nodetree.type != NTREE_COMPOSIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
LISTBASE_FOREACH (bNode *, node, &nodetree.nodes) {
|
||||
if (strcmp(node->idname, "CompositorNodeOutputFile") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeImageMultiFile *node_data = static_cast<NodeImageMultiFile *>(node->storage);
|
||||
version_escape_curly_braces(node_data->base_path, FILE_MAX);
|
||||
|
||||
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
|
||||
NodeImageMultiFileSocket *socket_data = static_cast<NodeImageMultiFileSocket *>(
|
||||
sock->storage);
|
||||
version_escape_curly_braces(socket_data->path, FILE_MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void do_versions_after_linking_450(FileData * /*fd*/, Main *bmain)
|
||||
{
|
||||
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 405, 8)) {
|
||||
@@ -4861,6 +4910,23 @@ void blo_do_versions_450(FileData * /*fd*/, Library * /*lib*/, Main *bmain)
|
||||
}
|
||||
}
|
||||
|
||||
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 405, 67)) {
|
||||
/* Version render output paths (both primary on scene as well as those in
|
||||
* the File Output compositor node) to escape curly braces. */
|
||||
{
|
||||
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
|
||||
version_escape_curly_braces(scene->r.pic, FILE_MAX);
|
||||
if (scene->nodetree) {
|
||||
version_escape_curly_braces_in_compositor_file_output_nodes(*scene->nodetree);
|
||||
}
|
||||
}
|
||||
|
||||
LISTBASE_FOREACH (bNodeTree *, nodetree, &bmain->nodetrees) {
|
||||
version_escape_curly_braces_in_compositor_file_output_nodes(*nodetree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Always run this versioning (keep at the bottom of the function). Meshes are written with the
|
||||
* legacy format which always needs to be converted to the new format on file load. To be moved
|
||||
* to a subversion check in 5.0. */
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "BKE_global.hh"
|
||||
#include "BKE_idprop.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_path_templates.hh"
|
||||
#include "BKE_screen.hh"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
@@ -1112,6 +1113,20 @@ static uiBut *ui_item_with_label(uiLayout *layout,
|
||||
but = uiDefAutoButR(block, ptr, prop, index, str, icon, x, y, prop_but_width, h);
|
||||
}
|
||||
|
||||
/* Highlight in red on path template validity errors. */
|
||||
if (but != nullptr && ELEM(but->type, UI_BTYPE_TEXT)) {
|
||||
/* We include PROP_NONE here because some plain string properties are used
|
||||
* as parts of paths. For example, the sub-paths in the compositor's File
|
||||
* Output node. */
|
||||
if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_NONE)) {
|
||||
if ((RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) {
|
||||
if (!BKE_validate_template_syntax(but->drawstr.c_str()).is_empty()) {
|
||||
UI_but_flag_enable(but, UI_BUT_REDALERT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flag & UI_ITEM_R_IMMEDIATE) {
|
||||
UI_but_flag_enable(but, UI_BUT_ACTIVATE_ON_INIT);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "BKE_library.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_paint.hh"
|
||||
#include "BKE_path_templates.hh"
|
||||
#include "BKE_screen.hh"
|
||||
#include "BKE_vfont.hh"
|
||||
|
||||
@@ -1043,13 +1044,15 @@ static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
|
||||
}
|
||||
}
|
||||
|
||||
/* Warn if relative paths are used when unsupported (will already display red-alert). */
|
||||
/* Warn on path validity errors. */
|
||||
if (ELEM(but->type, UI_BTYPE_TEXT) &&
|
||||
/* Check red-alert, if the flag is not set, then this was suppressed. */
|
||||
(but->flag & UI_BUT_REDALERT))
|
||||
{
|
||||
if (rnaprop) {
|
||||
PropertySubType subtype = RNA_property_subtype(rnaprop);
|
||||
|
||||
/* If relative paths are used when unsupported (will already display red-alert). */
|
||||
if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH)) {
|
||||
if ((RNA_property_flag(rnaprop) & PROP_PATH_SUPPORTS_BLEND_RELATIVE) == 0) {
|
||||
if (BLI_path_is_rel(but->drawstr.c_str())) {
|
||||
@@ -1062,6 +1065,27 @@ static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* We include PROP_NONE here because some plain string properties are used
|
||||
* as parts of paths. For example, the sub-paths in the compositor's File
|
||||
* Output node. */
|
||||
if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_NONE)) {
|
||||
/* Template parse errors, for paths that support it. */
|
||||
if ((RNA_property_flag(rnaprop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) {
|
||||
const blender::StringRef path = but->drawstr;
|
||||
const blender::Vector<blender::bke::path_templates::Error> errors =
|
||||
BKE_validate_template_syntax(path);
|
||||
|
||||
if (!errors.is_empty()) {
|
||||
std::string error_message("Syntax error(s):");
|
||||
for (const blender::bke::path_templates::Error &error : errors) {
|
||||
error_message += "\n - " + BKE_path_template_error_to_string(error, path);
|
||||
}
|
||||
UI_tooltip_text_field_add(
|
||||
*data, error_message, {}, UI_TIP_STYLE_NORMAL, UI_TIP_LC_ALERT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -924,14 +924,19 @@ static bool bake_targets_output_external(const BakeAPIRender *bkr,
|
||||
BakeData *bake = &bkr->scene->r.bake;
|
||||
char filepath[FILE_MAX];
|
||||
|
||||
BKE_image_path_from_imtype(filepath,
|
||||
bkr->filepath,
|
||||
BKE_main_blendfile_path(bkr->main),
|
||||
0,
|
||||
bake->im_format.imtype,
|
||||
true,
|
||||
false,
|
||||
nullptr);
|
||||
const blender::Vector<bke::path_templates::Error> errors = BKE_image_path_from_imtype(
|
||||
filepath,
|
||||
bkr->filepath,
|
||||
BKE_main_blendfile_path(bkr->main),
|
||||
nullptr,
|
||||
0,
|
||||
bake->im_format.imtype,
|
||||
true,
|
||||
false,
|
||||
nullptr);
|
||||
BLI_assert_msg(errors.is_empty(),
|
||||
"Path parsing errors should only occur when a variable map is provided.");
|
||||
UNUSED_VARS_NDEBUG(errors);
|
||||
|
||||
if (bkr->is_automatic_name) {
|
||||
BLI_path_suffix(filepath, FILE_MAX, ob->id.name + 2, "_");
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
|
||||
#include "render_intern.hh"
|
||||
|
||||
namespace path_templates = blender::bke::path_templates;
|
||||
|
||||
/* TODO(sergey): Find better approximation of the scheduled frames.
|
||||
* For really high-resolution renders it might fail still. */
|
||||
#define MAX_SCHEDULED_FRAMES 8
|
||||
@@ -415,20 +417,32 @@ static void screen_opengl_render_write(OGLRender *oglrender)
|
||||
|
||||
rr = RE_AcquireResultRead(oglrender->re);
|
||||
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
scene->r.pic,
|
||||
BKE_main_blendfile_path(oglrender->bmain),
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
false,
|
||||
nullptr);
|
||||
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
|
||||
&scene->r);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath,
|
||||
scene->r.pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
/* write images as individual images or stereo */
|
||||
BKE_render_result_stamp_info(scene, scene->camera, rr, false);
|
||||
ok = BKE_image_render_write(oglrender->reports, rr, scene, false, filepath);
|
||||
if (!errors.is_empty()) {
|
||||
std::unique_lock lock(oglrender->reports_mutex);
|
||||
BKE_report_path_template_errors(oglrender->reports, RPT_ERROR, scene->r.pic, errors);
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
/* write images as individual images or stereo */
|
||||
BKE_render_result_stamp_info(scene, scene->camera, rr, false);
|
||||
ok = BKE_image_render_write(oglrender->reports, rr, scene, false, filepath);
|
||||
|
||||
RE_ReleaseResultImage(oglrender->re);
|
||||
RE_ReleaseResultImage(oglrender->re);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
printf("OpenGL Render written to '%s'\n", filepath);
|
||||
@@ -1032,17 +1046,29 @@ static void write_result(TaskPool *__restrict pool, WriteTaskData *task_data)
|
||||
* calculate file name again here.
|
||||
*/
|
||||
char filepath[FILE_MAX];
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
scene->r.pic,
|
||||
BKE_main_blendfile_path(oglrender->bmain),
|
||||
cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
|
||||
&scene->r);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath,
|
||||
scene->r.pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
|
||||
if (!errors.is_empty()) {
|
||||
BKE_report_path_template_errors(&reports, RPT_ERROR, scene->r.pic, errors);
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
BKE_render_result_stamp_info(scene, scene->camera, rr, false);
|
||||
ok = BKE_image_render_write(nullptr, rr, scene, true, filepath);
|
||||
}
|
||||
|
||||
BKE_render_result_stamp_info(scene, scene->camera, rr, false);
|
||||
ok = BKE_image_render_write(nullptr, rr, scene, true, filepath);
|
||||
if (!ok) {
|
||||
BKE_reportf(&reports, RPT_ERROR, "Write error: cannot save %s", filepath);
|
||||
}
|
||||
@@ -1121,16 +1147,26 @@ static bool screen_opengl_render_anim_step(OGLRender *oglrender)
|
||||
is_movie = BKE_imtype_is_movie(scene->r.im_format.imtype);
|
||||
|
||||
if (!is_movie) {
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
scene->r.pic,
|
||||
BKE_main_blendfile_path(oglrender->bmain),
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
|
||||
&scene->r);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath,
|
||||
scene->r.pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
|
||||
if ((scene->r.mode & R_NO_OVERWRITE) && BLI_exists(filepath)) {
|
||||
if (!errors.is_empty()) {
|
||||
std::unique_lock lock(oglrender->reports_mutex);
|
||||
BKE_report_path_template_errors(oglrender->reports, RPT_ERROR, scene->r.pic, errors);
|
||||
ok = false;
|
||||
}
|
||||
else if ((scene->r.mode & R_NO_OVERWRITE) && BLI_exists(filepath)) {
|
||||
{
|
||||
std::unique_lock lock(oglrender->reports_mutex);
|
||||
BKE_reportf(oglrender->reports, RPT_INFO, "Skipping existing frame \"%s\"", filepath);
|
||||
|
||||
@@ -38,4 +38,5 @@ void MOV_write_end(MovieWriter *writer);
|
||||
void MOV_filepath_from_settings(char filepath[/*FILE_MAX*/ 1024],
|
||||
const RenderData *rd,
|
||||
bool preview,
|
||||
const char *suffix);
|
||||
const char *suffix,
|
||||
ReportList *reports);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
# include "BKE_global.hh"
|
||||
# include "BKE_image.hh"
|
||||
# include "BKE_main.hh"
|
||||
# include "BKE_path_templates.hh"
|
||||
|
||||
# include "IMB_imbuf.hh"
|
||||
|
||||
@@ -53,11 +54,12 @@ static void ffmpeg_dict_set_int(AVDictionary **dict, const char *key, int value)
|
||||
}
|
||||
|
||||
static void ffmpeg_movie_close(MovieWriter *context);
|
||||
static void ffmpeg_filepath_get(MovieWriter *context,
|
||||
static bool ffmpeg_filepath_get(MovieWriter *context,
|
||||
char filepath[FILE_MAX],
|
||||
const RenderData *rd,
|
||||
bool preview,
|
||||
const char *suffix);
|
||||
const char *suffix,
|
||||
ReportList *reports);
|
||||
|
||||
static AVFrame *alloc_frame(AVPixelFormat pix_fmt, int width, int height)
|
||||
{
|
||||
@@ -1015,7 +1017,9 @@ static bool start_ffmpeg_impl(MovieWriter *context,
|
||||
}
|
||||
|
||||
/* Determine the correct filename */
|
||||
ffmpeg_filepath_get(context, filepath, rd, context->ffmpeg_preview, suffix);
|
||||
if (!ffmpeg_filepath_get(context, filepath, rd, context->ffmpeg_preview, suffix, reports)) {
|
||||
return false;
|
||||
}
|
||||
FF_DEBUG_PRINT(
|
||||
"ffmpeg: starting output to %s:\n"
|
||||
" type=%d, codec=%d, audio_codec=%d,\n"
|
||||
@@ -1248,12 +1252,19 @@ static void flush_delayed_frames(AVCodecContext *c, AVStream *stream, AVFormatCo
|
||||
av_packet_free(&packet);
|
||||
}
|
||||
|
||||
/* Get the output filename-- similar to the other output formats */
|
||||
static void ffmpeg_filepath_get(MovieWriter *context,
|
||||
/**
|
||||
* Get the output filename-- similar to the other output formats.
|
||||
*
|
||||
* \param reports If non-null, will report errors with `RPT_ERROR` level reports.
|
||||
*
|
||||
* \return true on success, false on failure due to errors.
|
||||
*/
|
||||
static bool ffmpeg_filepath_get(MovieWriter *context,
|
||||
char filepath[FILE_MAX],
|
||||
const RenderData *rd,
|
||||
bool preview,
|
||||
const char *suffix)
|
||||
const char *suffix,
|
||||
ReportList *reports)
|
||||
{
|
||||
char autosplit[20];
|
||||
|
||||
@@ -1262,7 +1273,7 @@ static void ffmpeg_filepath_get(MovieWriter *context,
|
||||
int sfra, efra;
|
||||
|
||||
if (!filepath || !exts) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
@@ -1275,6 +1286,14 @@ static void ffmpeg_filepath_get(MovieWriter *context,
|
||||
}
|
||||
|
||||
BLI_strncpy(filepath, rd->pic, FILE_MAX);
|
||||
|
||||
const blender::Vector<blender::bke::path_templates::Error> errors = BKE_path_apply_template(
|
||||
filepath, FILE_MAX, BKE_build_template_variables(BKE_main_blendfile_path_from_global(), rd));
|
||||
if (!errors.is_empty()) {
|
||||
BKE_report_path_template_errors(reports, RPT_ERROR, filepath, errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
BLI_path_abs(filepath, BKE_main_blendfile_path_from_global());
|
||||
|
||||
BLI_file_ensure_parent_dir_exists(filepath);
|
||||
@@ -1316,14 +1335,17 @@ static void ffmpeg_filepath_get(MovieWriter *context,
|
||||
}
|
||||
|
||||
BLI_path_suffix(filepath, FILE_MAX, suffix, "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ffmpeg_get_filepath(char filepath[/*FILE_MAX*/ 1024],
|
||||
const RenderData *rd,
|
||||
bool preview,
|
||||
const char *suffix)
|
||||
const char *suffix,
|
||||
ReportList *reports)
|
||||
{
|
||||
ffmpeg_filepath_get(nullptr, filepath, rd, preview, suffix);
|
||||
ffmpeg_filepath_get(nullptr, filepath, rd, preview, suffix, reports);
|
||||
}
|
||||
|
||||
static MovieWriter *ffmpeg_movie_open(const Scene *scene,
|
||||
@@ -1552,15 +1574,16 @@ void MOV_write_end(MovieWriter *writer)
|
||||
void MOV_filepath_from_settings(char filepath[/*FILE_MAX*/ 1024],
|
||||
const RenderData *rd,
|
||||
bool preview,
|
||||
const char *suffix)
|
||||
const char *suffix,
|
||||
ReportList *reports)
|
||||
{
|
||||
#ifdef WITH_FFMPEG
|
||||
if (is_imtype_ffmpeg(rd->im_format.imtype)) {
|
||||
ffmpeg_get_filepath(filepath, rd, preview, suffix);
|
||||
ffmpeg_get_filepath(filepath, rd, preview, suffix, reports);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
UNUSED_VARS(rd, preview, suffix);
|
||||
UNUSED_VARS(rd, preview, suffix, reports);
|
||||
#endif
|
||||
filepath[0] = '\0';
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ enum PropertySubType {
|
||||
|
||||
/* Make sure enums are updated with these */
|
||||
/* HIGHEST FLAG IN USE: 1u << 31
|
||||
* FREE FLAGS: 13, 14. */
|
||||
* FREE FLAGS: 13. */
|
||||
enum PropertyFlag {
|
||||
/**
|
||||
* Editable means the property is editable in the user
|
||||
@@ -429,6 +429,11 @@ enum PropertyFlag {
|
||||
*/
|
||||
PROP_PATH_SUPPORTS_BLEND_RELATIVE = (1 << 15),
|
||||
|
||||
/**
|
||||
* Paths that are evaluated with templating.
|
||||
*/
|
||||
PROP_PATH_SUPPORTS_TEMPLATES = (1 << 14),
|
||||
|
||||
/** Do not write in presets (#PROP_HIDDEN and #PROP_SKIP_SAVE won't either). */
|
||||
PROP_SKIP_PRESET = (1 << 11),
|
||||
};
|
||||
|
||||
@@ -7724,6 +7724,7 @@ static void rna_def_cmp_output_file_slot_file(BlenderRNA *brna)
|
||||
RNA_def_struct_name_property(srna, prop);
|
||||
RNA_def_property_ui_text(prop, "Path", "Subpath used for this slot");
|
||||
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_EDITOR_FILEBROWSER);
|
||||
RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_TEMPLATES);
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr);
|
||||
}
|
||||
static void rna_def_cmp_output_file_slot_layer(BlenderRNA *brna)
|
||||
@@ -7797,7 +7798,8 @@ static void def_cmp_output_file(BlenderRNA *brna, StructRNA *srna)
|
||||
prop = RNA_def_property(srna, "base_path", PROP_STRING, PROP_FILEPATH);
|
||||
RNA_def_property_string_sdna(prop, nullptr, "base_path");
|
||||
RNA_def_property_ui_text(prop, "Base Path", "Base output path for the image");
|
||||
RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE);
|
||||
RNA_def_property_flag(
|
||||
prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES);
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
|
||||
|
||||
prop = RNA_def_property(srna, "active_input_index", PROP_INT, PROP_NONE);
|
||||
|
||||
@@ -175,6 +175,9 @@ static constexpr auto PROP_PATH_OUTPUT_DESCR = "";
|
||||
static constexpr auto PROP_PATH_RELATIVE_DESCR =
|
||||
"This path supports relative prefix \"//\" which is expanded the the directory "
|
||||
"where the current \".blend\" file is located.";
|
||||
static constexpr auto PROP_PATH_SUPPORTS_TEMPLATES_DESCR =
|
||||
"This path supports the \"{variable_name}\" template syntax, which substitutes the "
|
||||
"value of the referenced variable in place of the template expression";
|
||||
static constexpr auto PROP_ENUM_FLAG_DESCR = "";
|
||||
|
||||
const EnumPropertyItem rna_enum_property_flag_items[] = {
|
||||
@@ -199,6 +202,11 @@ const EnumPropertyItem rna_enum_property_flag_items[] = {
|
||||
0,
|
||||
"Relative Path Support",
|
||||
PROP_PATH_RELATIVE_DESCR},
|
||||
{PROP_PATH_SUPPORTS_TEMPLATES,
|
||||
"SUPPORTS_TEMPLATES",
|
||||
0,
|
||||
"Variable expression support",
|
||||
PROP_PATH_SUPPORTS_TEMPLATES_DESCR},
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
@@ -801,6 +809,12 @@ static bool rna_Property_is_path_supports_blend_relative_flag_get(PointerRNA *pt
|
||||
return (prop->flag & PROP_PATH_SUPPORTS_BLEND_RELATIVE) != 0;
|
||||
}
|
||||
|
||||
static bool rna_Property_is_path_supports_templates_flag_get(PointerRNA *ptr)
|
||||
{
|
||||
PropertyRNA *prop = (PropertyRNA *)ptr->data;
|
||||
return (prop->flag & PROP_PATH_SUPPORTS_TEMPLATES) != 0;
|
||||
}
|
||||
|
||||
static int rna_Property_tags_get(PointerRNA *ptr)
|
||||
{
|
||||
return RNA_property_tags(static_cast<PropertyRNA *>(ptr->data));
|
||||
@@ -3314,6 +3328,16 @@ static void rna_def_property(BlenderRNA *brna)
|
||||
"Property is a path which supports the \"//\" prefix, "
|
||||
"signifying the location as relative to the \".blend\" files directory");
|
||||
|
||||
prop = RNA_def_property(srna, "is_path_supports_templates", PROP_BOOLEAN, PROP_NONE);
|
||||
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
|
||||
RNA_def_property_boolean_funcs(
|
||||
prop, "rna_Property_is_path_supports_templates_flag_get", nullptr);
|
||||
RNA_def_property_ui_text(
|
||||
prop,
|
||||
"Variable Expression Support",
|
||||
"Property is a path which supports the \"{variable_name}\" variable expression syntax, "
|
||||
"which substitutes the value of the referenced variable in place of the expression");
|
||||
|
||||
prop = RNA_def_property(srna, "tags", PROP_ENUM, PROP_NONE);
|
||||
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
|
||||
RNA_def_property_enum_items(prop, dummy_prop_tags);
|
||||
|
||||
@@ -7251,7 +7251,8 @@ static void rna_def_scene_render_data(BlenderRNA *brna)
|
||||
"Output Path",
|
||||
"Directory/name to save animations, # characters define the position "
|
||||
"and padding of frame numbers");
|
||||
RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE);
|
||||
RNA_def_property_flag(
|
||||
prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES);
|
||||
RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr);
|
||||
|
||||
/* Render result EXR cache. */
|
||||
|
||||
@@ -97,9 +97,15 @@ static void rna_Scene_uvedit_aspect(Scene * /*scene*/, Object *ob, float aspect[
|
||||
aspect[0] = aspect[1] = 1.0f;
|
||||
}
|
||||
|
||||
static void rna_SceneRender_get_frame_path(
|
||||
RenderData *rd, Main *bmain, int frame, bool preview, const char *view, char *filepath)
|
||||
static void rna_SceneRender_get_frame_path(RenderData *rd,
|
||||
Main *bmain,
|
||||
ReportList *reports,
|
||||
int frame,
|
||||
bool preview,
|
||||
const char *view,
|
||||
char *filepath)
|
||||
{
|
||||
|
||||
const char *suffix = BKE_scene_multiview_view_suffix_get(rd, view);
|
||||
|
||||
/* avoid nullptr pointer */
|
||||
@@ -108,17 +114,27 @@ static void rna_SceneRender_get_frame_path(
|
||||
}
|
||||
|
||||
if (BKE_imtype_is_movie(rd->im_format.imtype)) {
|
||||
MOV_filepath_from_settings(filepath, rd, preview != 0, suffix);
|
||||
MOV_filepath_from_settings(filepath, rd, preview != 0, suffix, reports);
|
||||
}
|
||||
else {
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
rd->pic,
|
||||
BKE_main_blendfile_path(bmain),
|
||||
(frame == INT_MIN) ? rd->cfra : frame,
|
||||
&rd->im_format,
|
||||
(rd->scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
suffix);
|
||||
const char *relbase = BKE_main_blendfile_path(bmain);
|
||||
const blender::bke::path_templates::VariableMap template_variables =
|
||||
BKE_build_template_variables(relbase, rd);
|
||||
|
||||
const blender::Vector<blender::bke::path_templates::Error> errors =
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
rd->pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
(frame == INT_MIN) ? rd->cfra : frame,
|
||||
&rd->im_format,
|
||||
(rd->scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
suffix);
|
||||
|
||||
if (!errors.is_empty()) {
|
||||
BKE_report_path_template_errors(reports, RPT_ERROR, rd->pic, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +440,7 @@ void RNA_api_scene_render(StructRNA *srna)
|
||||
PropertyRNA *parm;
|
||||
|
||||
func = RNA_def_function(srna, "frame_path", "rna_SceneRender_get_frame_path");
|
||||
RNA_def_function_flag(func, FUNC_USE_MAIN);
|
||||
RNA_def_function_flag(func, FUNC_USE_MAIN | FUNC_USE_REPORTS);
|
||||
RNA_def_function_ui_description(
|
||||
func, "Return the absolute path to the filename to be written for a given frame");
|
||||
RNA_def_int(func,
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
|
||||
#include "node_composite_util.hh"
|
||||
|
||||
namespace path_templates = blender::bke::path_templates;
|
||||
|
||||
/* **************** OUTPUT FILE ******************** */
|
||||
|
||||
/* find unique path */
|
||||
@@ -539,7 +541,14 @@ class FileOutputOperation : public NodeOperation {
|
||||
|
||||
char base_path[FILE_MAX];
|
||||
const auto &socket = *static_cast<NodeImageMultiFileSocket *>(input->storage);
|
||||
get_single_layer_image_base_path(socket.path, base_path);
|
||||
|
||||
if (!get_single_layer_image_base_path(socket.path, base_path)) {
|
||||
/* TODO: propagate this error to the render pipeline and UI. */
|
||||
BKE_report(nullptr,
|
||||
RPT_ERROR,
|
||||
"Invalid path template in File Output node. Skipping writing file.");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* The image saving code expects EXR images to have a different structure than standard
|
||||
* images. In particular, in EXR images, the buffers need to be stored in passes that are, in
|
||||
@@ -584,7 +593,11 @@ class FileOutputOperation : public NodeOperation {
|
||||
* name does not contain a view suffix. */
|
||||
char image_path[FILE_MAX];
|
||||
const char *path_view = has_views ? "" : context().get_view_name().data();
|
||||
get_multi_layer_exr_image_path(base_path, path_view, image_path);
|
||||
|
||||
if (!get_multi_layer_exr_image_path(base_path, path_view, false, image_path)) {
|
||||
BLI_assert_unreachable();
|
||||
return;
|
||||
}
|
||||
|
||||
const int2 size = result.domain().size;
|
||||
FileOutput &file_output = context().render_context()->get_file_output(
|
||||
@@ -612,7 +625,12 @@ class FileOutputOperation : public NodeOperation {
|
||||
* sure the file name does not contain a view suffix. */
|
||||
char image_path[FILE_MAX];
|
||||
const char *write_view = store_views_in_single_file ? "" : view;
|
||||
get_multi_layer_exr_image_path(get_base_path(), write_view, image_path);
|
||||
if (!get_multi_layer_exr_image_path(get_base_path(), write_view, true, image_path)) {
|
||||
/* TODO: propagate this error to the render pipeline and UI. */
|
||||
BKE_report(
|
||||
nullptr, RPT_ERROR, "Invalid path template in File Output node. Skipping writing file.");
|
||||
return;
|
||||
}
|
||||
|
||||
const int2 size = compute_domain().size;
|
||||
const ImageFormatData format = node_storage(bnode()).format;
|
||||
@@ -845,29 +863,76 @@ class FileOutputOperation : public NodeOperation {
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the base path of the image to be saved, based on the base path of the node. The base name
|
||||
* is an optional initial name of the image, which will later be concatenated with other
|
||||
* information like the frame number, view, and extension. If the base name is empty, then the
|
||||
* base path represents a directory, so a trailing slash is ensured. */
|
||||
void get_single_layer_image_base_path(const char *base_name, char *base_path)
|
||||
/**
|
||||
* Get the base path of the image to be saved, based on the base path of the
|
||||
* node. The base name is an optional initial name of the image, which will
|
||||
* later be concatenated with other information like the frame number, view,
|
||||
* and extension. If the base name is empty, then the base path represents a
|
||||
* directory, so a trailing slash is ensured.
|
||||
*
|
||||
* Note: this takes care of path template expansion as well.
|
||||
*
|
||||
* If there are any errors processing the path, `bath_base` will be set to an
|
||||
* empty string.
|
||||
*
|
||||
* \return True on success, false if there were any errors processing the
|
||||
* path.
|
||||
*/
|
||||
bool get_single_layer_image_base_path(const char *base_name, char *r_base_path)
|
||||
{
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(
|
||||
BKE_main_blendfile_path_from_global(), &context().get_render_data());
|
||||
|
||||
/* Do template expansion on the node's base path. */
|
||||
char node_base_path[FILE_MAX] = "";
|
||||
BLI_strncpy(node_base_path, get_base_path(), FILE_MAX);
|
||||
{
|
||||
blender::Vector<path_templates::Error> errors = BKE_path_apply_template(
|
||||
node_base_path, FILE_MAX, template_variables);
|
||||
if (!errors.is_empty()) {
|
||||
r_base_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (base_name[0]) {
|
||||
BLI_path_join(base_path, FILE_MAX, get_base_path(), base_name);
|
||||
/* Do template expansion on the socket's sub path ("base name"). */
|
||||
char sub_path[FILE_MAX] = "";
|
||||
BLI_strncpy(sub_path, base_name, FILE_MAX);
|
||||
{
|
||||
blender::Vector<path_templates::Error> errors = BKE_path_apply_template(
|
||||
sub_path, FILE_MAX, template_variables);
|
||||
if (!errors.is_empty()) {
|
||||
r_base_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Combine the base path and sub path. */
|
||||
BLI_path_join(r_base_path, FILE_MAX, node_base_path, sub_path);
|
||||
}
|
||||
else {
|
||||
BLI_strncpy(base_path, get_base_path(), FILE_MAX);
|
||||
BLI_path_slash_ensure(base_path, FILE_MAX);
|
||||
/* Just use the base path, as a directory. */
|
||||
BLI_strncpy(r_base_path, node_base_path, FILE_MAX);
|
||||
BLI_path_slash_ensure(r_base_path, FILE_MAX);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Get the path of the image to be saved based on the given format. */
|
||||
void get_single_layer_image_path(const char *base_path,
|
||||
const ImageFormatData &format,
|
||||
char *image_path)
|
||||
char *r_image_path)
|
||||
{
|
||||
BKE_image_path_from_imformat(image_path,
|
||||
BKE_image_path_from_imformat(r_image_path,
|
||||
base_path,
|
||||
BKE_main_blendfile_path_from_global(),
|
||||
/* No variables, because path templating is
|
||||
* already done by
|
||||
* `get_single_layer_image_base_path()` before
|
||||
* this is called. */
|
||||
nullptr,
|
||||
context().get_frame_number(),
|
||||
&format,
|
||||
use_file_extension(),
|
||||
@@ -875,19 +940,47 @@ class FileOutputOperation : public NodeOperation {
|
||||
nullptr);
|
||||
}
|
||||
|
||||
/* Get the path of the EXR image to be saved. If the given view is not empty, its corresponding
|
||||
* file suffix will be appended to the name. */
|
||||
void get_multi_layer_exr_image_path(const char *base_path, const char *view, char *image_path)
|
||||
/**
|
||||
* Get the path of the EXR image to be saved. If the given view is not empty,
|
||||
* its corresponding file suffix will be appended to the name.
|
||||
*
|
||||
* If there are any errors processing the path, the resulting path will be
|
||||
* empty.
|
||||
*
|
||||
* \param apply_template Whether to run templating on the path or not. This is
|
||||
* needed because this function is called from more than one place, some of
|
||||
* which have already applied templating to the path and some of which
|
||||
* haven't. Double-applying templating can give incorrect results.
|
||||
*
|
||||
* \return True on success, false if there were any errors processing the
|
||||
* path.
|
||||
*/
|
||||
bool get_multi_layer_exr_image_path(const char *base_path,
|
||||
const char *view,
|
||||
const bool apply_template,
|
||||
char *r_image_path)
|
||||
{
|
||||
const char *suffix = BKE_scene_multiview_view_suffix_get(&context().get_render_data(), view);
|
||||
BKE_image_path_from_imtype(image_path,
|
||||
base_path,
|
||||
BKE_main_blendfile_path_from_global(),
|
||||
context().get_frame_number(),
|
||||
R_IMF_IMTYPE_MULTILAYER,
|
||||
use_file_extension(),
|
||||
true,
|
||||
suffix);
|
||||
const RenderData &render_data = context().get_render_data();
|
||||
const char *suffix = BKE_scene_multiview_view_suffix_get(&render_data, view);
|
||||
const char *relbase = BKE_main_blendfile_path_from_global();
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(
|
||||
relbase, &render_data);
|
||||
blender::Vector<path_templates::Error> errors = BKE_image_path_from_imtype(
|
||||
r_image_path,
|
||||
base_path,
|
||||
relbase,
|
||||
apply_template ? &template_variables : nullptr,
|
||||
context().get_frame_number(),
|
||||
R_IMF_IMTYPE_MULTILAYER,
|
||||
use_file_extension(),
|
||||
true,
|
||||
suffix);
|
||||
|
||||
if (!errors.is_empty()) {
|
||||
r_image_path[0] = '\0';
|
||||
}
|
||||
|
||||
return errors.is_empty();
|
||||
}
|
||||
|
||||
bool is_multi_layer()
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
#include "render_result.h"
|
||||
#include "render_types.h"
|
||||
|
||||
namespace path_templates = blender::bke::path_templates;
|
||||
|
||||
/* render flow
|
||||
*
|
||||
* 1) Initialize state
|
||||
@@ -2088,15 +2090,26 @@ void RE_RenderFrame(Render *re,
|
||||
}
|
||||
else {
|
||||
char filepath_override[FILE_MAX];
|
||||
BKE_image_path_from_imformat(filepath_override,
|
||||
rd.pic,
|
||||
BKE_main_blendfile_path(bmain),
|
||||
scene->r.cfra,
|
||||
&rd.im_format,
|
||||
(rd.scemode & R_EXTENSION) != 0,
|
||||
false,
|
||||
nullptr);
|
||||
do_write_image_or_movie(re, bmain, scene, 0, filepath_override);
|
||||
const char *relbase = BKE_main_blendfile_path(bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(
|
||||
relbase, &scene->r);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath_override,
|
||||
rd.pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
scene->r.cfra,
|
||||
&rd.im_format,
|
||||
(rd.scemode & R_EXTENSION) != 0,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
if (errors.is_empty()) {
|
||||
do_write_image_or_movie(re, bmain, scene, 0, filepath_override);
|
||||
}
|
||||
else {
|
||||
BKE_report_path_template_errors(re->reports, RPT_ERROR, rd.pic, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2309,18 +2322,29 @@ static bool do_write_image_or_movie(
|
||||
STRNCPY(filepath, filepath_override);
|
||||
}
|
||||
else {
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
scene->r.pic,
|
||||
BKE_main_blendfile_path(bmain),
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
const char *relbase = BKE_main_blendfile_path(bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(
|
||||
relbase, &scene->r);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath,
|
||||
scene->r.pic,
|
||||
relbase,
|
||||
&template_variables,
|
||||
scene->r.cfra,
|
||||
&scene->r.im_format,
|
||||
(scene->r.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
if (!errors.is_empty()) {
|
||||
BKE_report_path_template_errors(re->reports, RPT_ERROR, scene->r.pic, errors);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* write images as individual images or stereo */
|
||||
ok = BKE_image_render_write(re->reports, &rres, scene, true, filepath);
|
||||
if (ok) {
|
||||
ok = BKE_image_render_write(re->reports, &rres, scene, true, filepath);
|
||||
}
|
||||
}
|
||||
|
||||
RE_ReleaseResultImageViews(re, &rres);
|
||||
@@ -2332,7 +2356,7 @@ static bool do_write_image_or_movie(
|
||||
BLI_timecode_string_from_time_simple(filepath, sizeof(filepath), re->i.lastframetime);
|
||||
std::string message = fmt::format("Time: {}", filepath);
|
||||
|
||||
if (do_write_file) {
|
||||
if (do_write_file && ok) {
|
||||
BLI_timecode_string_from_time_simple(
|
||||
filepath, sizeof(filepath), re->i.lastframetime - render_time);
|
||||
message = fmt::format("{} (Saving: {})", message, filepath);
|
||||
@@ -2501,15 +2525,28 @@ void RE_RenderAnim(Render *re,
|
||||
|
||||
/* Touch/NoOverwrite options are only valid for image's */
|
||||
if (is_movie == false && do_write_file) {
|
||||
if (rd.mode & (R_NO_OVERWRITE | R_TOUCH)) {
|
||||
BKE_image_path_from_imformat(filepath,
|
||||
rd.pic,
|
||||
BKE_main_blendfile_path(bmain),
|
||||
scene->r.cfra,
|
||||
&rd.im_format,
|
||||
(rd.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
const char *relbase = BKE_main_blendfile_path(bmain);
|
||||
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
|
||||
&rd);
|
||||
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
|
||||
filepath,
|
||||
rd.pic,
|
||||
BKE_main_blendfile_path(bmain),
|
||||
&template_variables,
|
||||
scene->r.cfra,
|
||||
&rd.im_format,
|
||||
(rd.scemode & R_EXTENSION) != 0,
|
||||
true,
|
||||
nullptr);
|
||||
|
||||
/* The filepath cannot be parsed, so we can't save the renders anywhere.
|
||||
* So we just cancel. */
|
||||
if (!errors.is_empty()) {
|
||||
BKE_report_path_template_errors(re->reports, RPT_ERROR, rd.pic, errors);
|
||||
/* We have to set the `is_break` flag here so that final cleanup code
|
||||
* recognizes that the render has failed. */
|
||||
G.is_break = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (rd.mode & R_NO_OVERWRITE) {
|
||||
|
||||
@@ -1919,6 +1919,9 @@ static const char arg_handle_output_set_doc[] =
|
||||
"\tSet the render path and file name.\n"
|
||||
"\tUse '//' at the start of the path to render relative to the blend-file.\n"
|
||||
"\n"
|
||||
"\tYou can use path templating features such as '{blend_name}' in the path.\n"
|
||||
"\tSee Blender's documentation on path templates for more details.\n"
|
||||
"\n"
|
||||
"\tThe '#' characters are replaced by the frame number, and used to define zero padding.\n"
|
||||
"\n"
|
||||
"\t* 'animation_##_test.png' becomes 'animation_01_test.png'\n"
|
||||
|
||||
Reference in New Issue
Block a user