recorder writes video frames to folder

This commit is contained in:
Frank DeMarco 2019-05-16 03:51:36 -04:00
parent 1dbb2a2e1d
commit 321d9df1be
18 changed files with 325 additions and 161 deletions

View File

@ -1,8 +1,9 @@
/*** /***
reset, pause, auto reset, analog d-pad, gamepad config, any key, confirm exit reset, pause, auto reset, analog d-pad, gamepad config, any key, confirm exit
game, screen wipes game, screen wipes, screen offset, screen scale
:) SWEATY HANDS :) OILY SNACKS :) AND BAD HYGIENE :) :) SWEATY HANDS :) OILY SNACKS :) AND BAD HYGIENE :)
*surf's up broccoli* <it's surfing time but it's treacherous>
***/ ***/
@ -65,56 +66,6 @@ int link_shader(GLuint program)
return 0; return 0;
} }
SDL_Surface* get_screen_surface(SDL_Window *window)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
unsigned char* pixels = new unsigned char[24 * w * h];
GLenum format;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
format = GL_RGB;
#else
format = GL_BGR;
#endif
glReadPixels(0, 0, w, h, format, GL_UNSIGNED_BYTE, pixels);
SDL_Surface *surface = zoomSurface(
SDL_CreateRGBSurfaceFrom(pixels, w, h, 24, 3 * w,
0, 0, 0, 0), 1, -1, SMOOTHING_OFF);
delete[] pixels;
return surface;
}
void capture_screen(SDL_Window *window)
{
SDL_Surface *surface = get_screen_surface(window);
IMG_SavePNG(surface, "screen.png");
printf("saved png to screen.png\n");
SDL_FreeSurface(surface);
}
void start_recording(bool *is_recording)
{
*is_recording = true;
printf("start recording\n");
}
void end_recording(std::list<SDL_Surface*> frames, bool *is_recording)
{
*is_recording = false;
printf("end recording\n");
SDL_Surface *frame;
int ii = 0;
while (not frames.empty())
{
frame = frames.front();
char path[22];
sprintf(path, "frames/%03i.png", ii++);
IMG_SavePNG(frame, path);
frames.pop_front();
SDL_FreeSurface(frame);
}
}
GLuint get_gl_texture_from_surface(SDL_Surface *surface, GLint mipmap_filter) GLuint get_gl_texture_from_surface(SDL_Surface *surface, GLint mipmap_filter)
{ {
GLuint id; GLuint id;
@ -162,12 +113,9 @@ struct Demo : Game
{ {
SDL_Texture *grass_texture; SDL_Texture *grass_texture;
int recording_capture_framerate = 100, frame_time_overflow = 0, int frame_count = 0, frame_count_timestamp;
capture_time_overflow = 0, frame_count = 0, frame_count_timestamp, bool right_active = false, down_active = false, left_active = false,
last_capture_timestamp; up_active = false;
std::list<SDL_Surface*> frames;
bool is_recording = false, right_active = false, down_active = false,
left_active = false, up_active = false;
SDL_Event event; SDL_Event event;
GLuint vbo, space_texture_id, mvp_id, framerate_texture_id, flat_program, GLuint vbo, space_texture_id, mvp_id, framerate_texture_id, flat_program,
world_program, fake_texture_id; world_program, fake_texture_id;
@ -341,7 +289,7 @@ struct Demo : Game
glBindTexture(GL_TEXTURE_2D, space_texture_id); glBindTexture(GL_TEXTURE_2D, space_texture_id);
glUniform1i(sampler_uniform_id, 0); glUniform1i(sampler_uniform_id, 0);
glDepthFunc(GL_LESS); glDepthFunc(GL_LESS);
frame_count_timestamp = last_capture_timestamp = SDL_GetTicks(); frame_count_timestamp = SDL_GetTicks();
framerate_texture_id = get_gl_texture_from_surface( framerate_texture_id = get_gl_texture_from_surface(
get_framerate_indicator_surface(frame_count), GL_LINEAR); get_framerate_indicator_surface(frame_count), GL_LINEAR);
} }
@ -367,18 +315,7 @@ struct Demo : Game
// { // {
// if (event.type == SDL_KEYDOWN) // if (event.type == SDL_KEYDOWN)
// { // {
// if (event.key.keysym.sym == SDLK_F10) // if (event.key.keysym.sym == SDLK_F11)
// {
// if (not is_recording)
// {
// start_recording(&is_recording);
// }
// else
// {
// end_recording(frames, &is_recording);
// }
// }
// else if (event.key.keysym.sym == SDLK_F11)
// { // {
// if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) // if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN)
// { // {
@ -441,20 +378,6 @@ struct Demo : Game
// } // }
// } // }
// } // }
if (is_recording and ticks - last_capture_timestamp + capture_time_overflow >
recording_capture_framerate)
{
frames.push_back(get_screen_surface(window));
printf("added frame at %i\n", ticks);
capture_time_overflow = ticks - last_capture_timestamp + capture_time_overflow -
recording_capture_framerate;
last_capture_timestamp = ticks;
for (int ii = 1; capture_time_overflow > recording_capture_framerate;
ii++, capture_time_overflow -= recording_capture_framerate)
{
fprintf(stderr, "lost %i frame(s) during capture\n", ii);
}
}
if (is_gl_context) if (is_gl_context)
{ {
// glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);

View File

@ -36,18 +36,21 @@ $(SDLGFX2_DIR)%.o: $(SDLGFX2_DIR)%.c $(SDLGFX2_DIR)%.h
$(GLEW_DIR)%.o: $(GLEW_DIR)%.c $(GLEW_DIR)%.h $(GLEW_DIR)%.o: $(GLEW_DIR)%.c $(GLEW_DIR)%.h
$(CC_LINUX) $(CFLAGS) $< -o $@ $(CC_LINUX) $(CFLAGS) $< -o $@
$(SFW_SRC_DIR)Sprite.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Location.*pp) $(SFW_SRC_DIR)Sprite.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Location.*pp Node.*pp)
$(SFW_SRC_DIR)Game.o: $(addprefix $(SFW_SRC_DIR),Sprite.*pp Configuration.*pp Delegate.*pp Display.*pp Recorder.*pp) $(SFW_SRC_DIR)Game.o: $(addprefix $(SFW_SRC_DIR),Sprite.*pp Configuration.*pp Delegate.*pp Display.*pp \
$(SFW_SRC_DIR)Node.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Configuration.*pp) Recorder.*pp Node.*pp)
$(SFW_SRC_DIR)Recorder.o: $(addprefix $(SFW_SRC_DIR),extension.*pp) $(SFW_SRC_DIR)Node.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Configuration.*pp Node.*pp)
$(SFW_SRC_DIR)%.o: $(addprefix $(SFW_SRC_DIR),%.cpp %.hpp Node.*pp) $(SFW_SRC_DIR)Animation.o: $(addprefix $(SFW_SRC_DIR),Timer.*pp)
$(SFW_SRC_DIR)Recorder.o: $(addprefix $(SFW_SRC_DIR),extension.*pp Node.*pp)
$(SFW_SRC_DIR)%.o: $(addprefix $(SFW_SRC_DIR),%.cpp %.hpp)
$(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@ $(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@
Demo.o: Demo.cpp Demo.hpp $(addprefix $(SFW_SRC_DIR),Sprite.*pp Node.*pp Game.*pp Location.*pp Input.*pp Recorder.*pp) Demo.o: Demo.cpp Demo.hpp $(addprefix $(SFW_SRC_DIR),Sprite.*pp Node.*pp Game.*pp Location.*pp Input.*pp \
Recorder.*pp Timer.*pp Animation.*pp extension.*pp)
$(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@ $(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@
linux: Demo.o $(addprefix $(SFW_SRC_DIR),Sprite.o Node.o Game.o Location.o Configuration.o Input.o Delegate.o \ linux: Demo.o $(addprefix $(SFW_SRC_DIR),Sprite.o Node.o Game.o Location.o Configuration.o Input.o Delegate.o \
Display.o Recorder.o extension.o) \ Display.o Recorder.o Timer.o Animation.o extension.o) \
$(GLEW_DIR)glew.o $(addprefix $(SDLGFX2_DIR),SDL2_rotozoom.o SDL2_gfxPrimitives.o) $(GLEW_DIR)glew.o $(addprefix $(SDLGFX2_DIR),SDL2_rotozoom.o SDL2_gfxPrimitives.o)
$(CPPC_LINUX) $(LFLAGS) -D__LINUX__ $^ -lGL -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lstdc++fs -o demo $(CPPC_LINUX) $(LFLAGS) -D__LINUX__ $^ -lGL -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lstdc++fs -o demo

View File

@ -5,7 +5,8 @@
}, },
"path": "path":
{ {
"screenshots": "local/screenshots" "screenshots": "local/screenshots",
"video": "local/video"
}, },
"gamepad": "gamepad":
{ {

70
src/Animation.cpp Normal file
View File

@ -0,0 +1,70 @@
#include "Animation.hpp"
void Animation::play(int delay, bool play_once)
{
this->delay = delay;
playing = true;
paused = false;
previous_step_time = timer.elapsed;
overflow = 0;
count = 0;
ending = play_once;
if (delay <= 0)
{
timer.toggle(true);
}
}
void Animation::play_once(int delay)
{
play(delay, true);
}
void Animation::pause()
{
timer.toggle(false);
paused = true;
}
void Animation::unpause()
{
timer.toggle(true);
paused = false;
}
void Animation::reset()
{
timer.toggle(false);
playing = false;
timer.reset();
}
void Animation::update()
{
timer.update();
if (playing and not paused)
{
if (delay > 0)
{
delay -= timer.frame_duration;
if (delay <= 0)
{
timer.toggle(true);
}
}
if (delay <= 0)
{
if (timer.elapsed - previous_step_time + overflow > framerate)
{
overflow = timer.elapsed - previous_step_time + overflow - framerate;
previous_step_time = timer.elapsed;
step();
count++;
if (ending)
{
reset();
}
}
}
}
}

36
src/Animation.hpp Normal file
View File

@ -0,0 +1,36 @@
#ifndef Animation_h_
#define Animation_h_
#include <vector>
#include <functional>
#include <algorithm>
#include "Timer.hpp"
typedef std::function<void()> callback;
struct Animation
{
bool playing = false, ending = false, paused = false;
int previous_step_time = 0, delay = 0, overflow = 0, count = 0, framerate;
callback step;
Timer timer = Timer();
template<typename T>
Animation(void(T::*f)(), T* o, int framerate = 0) : framerate(framerate)
{
step = std::bind(f, o);
timer.toggle(false);
}
void play(int = 0, bool = false);
void play_once(int = 0);
void pause();
void unpause();
void reset();
void update();
};
#endif

View File

@ -15,20 +15,24 @@ void Configuration::set_defaults()
sys_config["keys"] = { sys_config["keys"] = {
{"record", {"CTRL", "SHIFT", "f10"}}, {"record", {"CTRL", "SHIFT", "f10"}},
{"screenshot", "f9"}, {"screenshot", "f9"},
{"action", " "}, {"action", "space"},
{"up", "up"}, {"up", "up"},
{"right", "right"}, {"right", "right"},
{"down", "down"}, {"down", "down"},
{"left", "left"} {"left", "left"},
{"pause", "enter"},
{"fullscreen", {"ALT", "enter"}}
}; };
sys_config["path"] = { sys_config["path"] = {
{"screenshots", "."} {"screenshots", "."},
{"video", "."}
}; };
sys_config["display"] = { sys_config["display"] = {
{"dimensions", {640, 480}}, {"dimensions", {640, 480}},
{"screenshot-prefix", "screenshot-"}, {"screenshot-prefix", "screenshot-"},
{"screenshot-extension", ".png"}, {"screenshot-extension", ".png"},
{"screenshot-zfill", 5} {"screenshot-zfill", 5},
{"recording-framerate", 100}
}; };
} }

View File

@ -9,3 +9,27 @@ glm::ivec2 Display::get_window_size()
SDL_GetWindowSize(get_root()->window, &size.x, &size.y); SDL_GetWindowSize(get_root()->window, &size.x, &size.y);
return size; return size;
} }
void Display::get_screen_pixels(unsigned char* pixels, int w, int h, int x, int y)
{
GLenum format;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
format = GL_RGB;
#else
format = GL_BGR;
#endif
glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels);
}
SDL_Surface* Display::get_screen_surface()
{
glm::ivec2 size = get_window_size();
unsigned char* pixels = new unsigned char[bpp / 8 * size.x * size.y];
get_screen_pixels(pixels, size.x, size.y);
SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(
pixels, size.x, size.y, bpp, bpp / 8 * size.x, 0, 0, 0, 0);
SDL_Surface* zoomed_surface = zoomSurface(surface, 1, -1, SMOOTHING_OFF);
SDL_FreeSurface(surface);
delete[] pixels;
return zoomed_surface;
}

View File

@ -4,6 +4,14 @@
#define GLM_ENABLE_EXPERIMENTAL #define GLM_ENABLE_EXPERIMENTAL
#include "glm/vec2.hpp" #include "glm/vec2.hpp"
#define GL_GLEXT_PROTOTYPES
#define GLEW_STATIC
#include "glew/glew.h"
#include <SDL_image.h>
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
#include "sdl2-gfx/SDL2_rotozoom.h"
#include "SDL.h" #include "SDL.h"
#include "Node.hpp" #include "Node.hpp"
@ -11,8 +19,12 @@
struct Display : Node struct Display : Node
{ {
const static int bpp = 24;
Display(Node*); Display(Node*);
glm::ivec2 get_window_size(); glm::ivec2 get_window_size();
SDL_Surface* get_screen_surface();
void get_screen_pixels(unsigned char*, int, int, int = 0, int = 0);
}; };

View File

@ -130,6 +130,7 @@ void Game::run()
last_frame_length = ticks - last_frame_timestamp; last_frame_length = ticks - last_frame_timestamp;
frame_time_overflow = last_frame_length + frame_time_overflow - frame_length; frame_time_overflow = last_frame_length + frame_time_overflow - frame_length;
last_frame_timestamp = ticks; last_frame_timestamp = ticks;
recorder->update();
delegate->dispatch(); delegate->dispatch();
update(); update();
} }

View File

@ -40,7 +40,9 @@ struct Input : Node
{"f9", SDLK_F9}, {"f9", SDLK_F9},
{"f10", SDLK_F10}, {"f10", SDLK_F10},
{"f11", SDLK_F11}, {"f11", SDLK_F11},
{"f12", SDLK_F11} {"f12", SDLK_F11},
{"enter", SDLK_RETURN},
{"space", SDLK_SPACE}
}; };
std::vector<KeyCombination> key_map; std::vector<KeyCombination> key_map;

View File

@ -15,16 +15,6 @@ nlohmann::json& Node::get_configuration()
return get_root()->configuration->config; return get_root()->configuration->config;
} }
Delegate* Node::get_delegate()
{
return get_root()->delegate;
}
Display* Node::get_display()
{
return get_root()->display;
}
Game* Node::get_root() Game* Node::get_root()
{ {
Node *current = this; Node *current = this;
@ -35,6 +25,16 @@ Game* Node::get_root()
return static_cast<Game*>(current); return static_cast<Game*>(current);
} }
Delegate* Node::get_delegate()
{
return get_root()->delegate;
}
Display* Node::get_display()
{
return get_root()->display;
}
void Node::print_branch() void Node::print_branch()
{ {
Node *current = this; Node *current = this;

View File

@ -11,6 +11,7 @@
struct Game; struct Game;
struct Delegate; struct Delegate;
struct Display; struct Display;
struct TimeFilter;
struct Node struct Node
{ {

View File

@ -11,65 +11,71 @@ void Recorder::respond(SDL_Event& event)
{ {
capture_screen(); capture_screen();
} }
else if (get_delegate()->compare(event, "record"))
{
if (animation.playing)
{
end_recording();
}
else
{
start_recording();
}
}
} }
void Recorder::capture_screen() void Recorder::capture_screen()
{ {
nlohmann::json config = get_configuration(); nlohmann::json config = get_configuration();
SDL_Surface* surface = get_screen_surface(); SDL_Surface* surface = get_display()->get_screen_surface();
fs::path directory = config["path"]["screenshots"]; fs::path directory = config["path"]["screenshots"];
fs::create_directories(directory); fs::create_directories(directory);
std::string prefix = config["display"]["screenshot-prefix"]. std::string prefix = config["display"]["screenshot-prefix"].
get<std::string>(); get<std::string>();
std::string extension = config["display"]["screenshot-extension"]. std::string extension = config["display"]["screenshot-extension"].
get<std::string>(); get<std::string>();
std::stringstream file_pattern;
file_pattern << prefix << "(.*)" << extension;
fs::path query = directory / file_pattern.str();
std::vector<fs::path> files = sfw::glob(query);
int zfill = config["display"]["screenshot-zfill"].get<int>(); int zfill = config["display"]["screenshot-zfill"].get<int>();
int index = 1; fs::path path = sfw::get_next_file_name(directory, zfill, prefix, extension);
if (files.size())
{
const std::string last = files.back().string();
std::smatch matches;
std::regex_match(last, matches, std::regex(query.string()));
index = std::stoi(matches[1]) + 1;
}
std::stringstream filename;
fs::path path;
do
{
filename << prefix << sfw::pad(index++, zfill) << extension;
path = directory / filename.str();
filename.str("");
filename.clear();
}
while (fs::exists(path));
IMG_SavePNG(surface, path.c_str()); IMG_SavePNG(surface, path.c_str());
std::cout << "screenshot saved to " << path.string() << std::endl; SDL_FreeSurface(surface);
std::cout << "Saved screenshot to " << path.string() << std::endl;
} }
SDL_Surface* Recorder::get_screen_surface() void Recorder::start_recording()
{ {
glm::ivec2 size = get_display()->get_window_size(); std::cout << "Starting recording..." << std::endl;
unsigned char* pixels = new unsigned char[24 * size.x * size.y]; animation.play();
get_screen_pixels(pixels, size.x, size.y);
SDL_Surface* surface = zoomSurface(
SDL_CreateRGBSurfaceFrom(
pixels, size.x, size.y, 24, 3 * size.x, 0, 0, 0, 0),
1, -1, SMOOTHING_OFF);
delete[] pixels;
return surface;
} }
void Recorder::get_screen_pixels(unsigned char* pixels, int w, int h, int x, int y) void Recorder::add_frame_to_video()
{ {
GLenum format; frames.push_back(get_display()->get_screen_surface());
#if SDL_BYTEORDER == SDL_BIG_ENDIAN }
format = GL_RGB;
#else void Recorder::end_recording()
format = GL_BGR; {
#endif std::cout << "Ending recording..." << std::endl;
glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels); animation.reset();
SDL_Surface* frame;
nlohmann::json config = get_configuration();
fs::path root = config["path"]["video"];
fs::create_directories(root);
fs::path directory = sfw::get_next_file_name(root, 5, "video-");
fs::create_directories(directory);
std::cout << "Writing recording to " << directory << "..." << std::endl;
for (int ii = 0; not frames.empty(); ii++)
{
frame = frames.front();
std::stringstream name;
name << sfw::pad(ii, 5) << ".png";
fs::path path = directory / name.str();
IMG_SavePNG(frame, path.string().c_str());
frames.erase(frames.begin());
SDL_FreeSurface(frame);
}
}
void Recorder::update()
{
animation.update();
} }

View File

@ -9,18 +9,10 @@
#define GLM_ENABLE_EXPERIMENTAL #define GLM_ENABLE_EXPERIMENTAL
#include "glm/ext.hpp" #include "glm/ext.hpp"
#define GL_GLEXT_PROTOTYPES
#define GLEW_STATIC
#include "glew/glew.h"
#include <SDL_image.h>
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
#include "sdl2-gfx/SDL2_rotozoom.h"
#include "json/json.hpp" #include "json/json.hpp"
#include "filesystem.hpp" #include "filesystem.hpp"
#include "Node.hpp" #include "Animation.hpp"
#include "Delegate.hpp" #include "Delegate.hpp"
#include "Display.hpp" #include "Display.hpp"
#include "extension.hpp" #include "extension.hpp"
@ -28,11 +20,17 @@
struct Recorder : Node struct Recorder : Node
{ {
std::vector<SDL_Surface*> frames;
Animation animation = Animation(&Recorder::add_frame_to_video, this, 100);
Recorder(Node*); Recorder(Node*);
void respond(SDL_Event&); void respond(SDL_Event&);
void capture_screen(); void capture_screen();
SDL_Surface* get_screen_surface(); void start_recording();
void get_screen_pixels(unsigned char*, int, int, int = 0, int = 0); void add_frame_to_video();
void end_recording();
void update();
std::string get_class_name() { return "Recorder"; }
}; };

33
src/Timer.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "Timer.hpp"
Timer::Timer()
{
ticks = SDL_GetTicks();
ticks_previous = ticks;
}
void Timer::toggle()
{
is_timing = not is_timing;
}
void Timer::toggle(bool state)
{
is_timing = state;
}
void Timer::reset()
{
elapsed = 0;
}
void Timer::update()
{
ticks = SDL_GetTicks();
frame_duration = ticks - ticks_previous;
if (is_timing)
{
elapsed += frame_duration;
}
ticks_previous = ticks;
}

21
src/Timer.hpp Normal file
View File

@ -0,0 +1,21 @@
#ifndef Timer_h_
#define Timer_h_
#include "SDL.h"
struct Timer
{
int ticks, ticks_previous, frame_duration = 0, elapsed = 0;
bool is_timing = true;
Timer();
void toggle();
void toggle(bool);
void reset();
void update();
};
#endif

View File

@ -9,7 +9,6 @@ std::vector<fs::path> sfw::glob(fs::path query)
} }
std::regex expression(query.string()); std::regex expression(query.string());
std::vector<fs::path> files; std::vector<fs::path> files;
std::cout << basename << " " << query << std::endl;
for (auto& entry: fs::directory_iterator(basename)) for (auto& entry: fs::directory_iterator(basename))
{ {
if (std::regex_match(entry.path().string(), expression)) if (std::regex_match(entry.path().string(), expression))
@ -20,3 +19,31 @@ std::vector<fs::path> sfw::glob(fs::path query)
std::sort(files.begin(), files.end()); std::sort(files.begin(), files.end());
return files; return files;
} }
fs::path sfw::get_next_file_name(
fs::path directory, int zfill, std::string prefix, std::string extension)
{
std::stringstream file_pattern;
file_pattern << prefix << "([0-9]+)" << extension;
fs::path query = directory / file_pattern.str();
std::vector<fs::path> files = sfw::glob(query);
int index = 1;
if (files.size())
{
const std::string last = files.back().string();
std::smatch matches;
std::regex_match(last, matches, std::regex(query.string()));
index = std::stoi(matches[1]) + 1;
}
std::stringstream filename;
fs::path path;
do
{
filename << prefix << sfw::pad(index++, zfill) << extension;
path = directory / filename.str();
filename.str("");
filename.clear();
}
while (fs::exists(path));
return path;
}

View File

@ -13,6 +13,8 @@
namespace sfw namespace sfw
{ {
std::vector<fs::path> glob(fs::path); std::vector<fs::path> glob(fs::path);
fs::path get_next_file_name(
fs::path, int = 0, std::string = "", std::string = "");
template<typename T> template<typename T>
std::string pad(T end, int width, char fill = '0') std::string pad(T end, int width, char fill = '0')