1 // Copyright 2016-2021 Doug Moen
2 // Licensed under the Apache License, version 2.0
3 // See accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0
4
5 #include "export.h"
6
7 #include <libcurv/io/compiled_shape.h>
8 #include <libcurv/io/png.h>
9 #include <libcurv/viewer/viewer.h>
10
11 #include <libcurv/context.h>
12 #include <libcurv/exception.h>
13 #include <libcurv/filesystem.h>
14 #include <libcurv/format.h>
15 #include <libcurv/render.h>
16 #include <libcurv/gpu_program.h>
17 #include <libcurv/json.h>
18 #include <libcurv/program.h>
19 #include <libcurv/range.h>
20 #include <libcurv/shape.h>
21 #include <libcurv/source.h>
22 #include <libcurv/viewed_shape.h>
23
24 #include <glm/vec2.hpp>
25
26 #include <algorithm>
27 #include <climits>
28 #include <cstdio>
29 #include <cstdlib>
30 #include <cstring>
31 #include <fstream>
32 #include <sstream>
33
34 using namespace curv;
35
eval(Value defl)36 Value Param::eval(Value defl)
37 {
38 if (value_.opt) {
39 auto src = make<String_Source>("",
40 stringify("-O ",name_,"=",value_.opt));
41 Program prog(params_.system_);
42 prog.compile(src, Scanner_Opts().skip_prefix(4+name_.size()));
43 auto nub = nub_phrase(prog.phrase_);
44 loc_ = nub->location();
45 if (isa<const Empty_Phrase>(nub)) {
46 if (!defl.is_missing())
47 return defl;
48 else
49 throw Exception(At_Program(prog), "missing argument");
50 }
51 return prog.eval();
52 } else {
53 loc_ = Src_Loc{make<Source>(params_.config_.filename_), Token{}};
54 return value_.config;
55 }
56 }
57
to_int(int lo,int hi)58 int Param::to_int(int lo, int hi)
59 {
60 return eval().to_int(lo, hi, *this);
61 }
62
to_double()63 double Param::to_double()
64 {
65 return eval().to_num(*this);
66 }
67
to_double(double defl)68 double Param::to_double(double defl)
69 {
70 return eval(defl).to_num(*this);
71 }
72
to_vec3()73 glm::dvec3 Param::to_vec3()
74 {
75 glm::dvec3 result;
76 auto list = eval().to<List>(*this);
77 list->assert_size(3, *this);
78 result.x = list->at(0).to_num(At_Index(0, *this));
79 result.y = list->at(1).to_num(At_Index(1, *this));
80 result.z = list->at(2).to_num(At_Index(2, *this));
81 return result;
82 }
83
to_bool()84 bool Param::to_bool()
85 {
86 return eval(true).to_bool(*this);
87 }
88
to_symbol()89 curv::Symbol_Ref Param::to_symbol()
90 {
91 auto val = eval();
92 return value_to_symbol(val, *this);
93 }
94
to_enum(const std::vector<const char * > & e)95 int Param::to_enum(const std::vector<const char*>& e)
96 {
97 auto val = eval();
98 return value_to_enum(val, e, *this);
99 }
100
unknown_parameter() const101 void Param::unknown_parameter() const
102 {
103 if (value_.opt) {
104 Src_Loc loc{
105 make<String_Source>("", stringify("-O ",name_,"=",value_.opt)),
106 {3, unsigned(3+name_.size())}};
107 if (params_.format_.empty()) {
108 throw Exception(At_Token{loc, params_.system_}, stringify(
109 "'",name_,"': Unknown -O parameter.\n"
110 "Use 'curv --help' for help."));
111 } else {
112 throw Exception(At_Token{loc, params_.system_}, stringify(
113 "'",name_,"': "
114 "Unknown -O parameter for output format '",params_.format_,"'.\n"
115 "Use 'curv --help -o ",params_.format_,"' for help."));
116 }
117 }
118 }
119
get_locations(std::list<Func_Loc> & locs) const120 void Param::get_locations(std::list<Func_Loc>& locs) const
121 {
122 locs.emplace_back(loc_);
123 }
124
rewrite_message(Shared<const String> msg) const125 Shared<const String> Param::rewrite_message(Shared<const String> msg) const
126 {
127 if (value_.opt) {
128 return msg;
129 } else {
130 return stringify(
131 "at field .",params_.config_.branchname_,
132 ".",make_symbol(name_),": ",msg);
133 }
134 }
135
system() const136 System& Param::system() const { return params_.system_; }
frame() const137 Frame* Param::frame() const { return nullptr; }
138
Export_Params(Options options,const Config & config,curv::System & sys)139 Export_Params::Export_Params(
140 Options options,
141 const Config& config,
142 curv::System& sys)
143 :
144 config_(config),
145 system_(sys)
146 {
147 for (auto& opt : options)
148 map_[opt.first] = {opt.second, curv::missing};
149 if (config.branch_) {
150 config.branch_->each_field(At_System(sys),
151 [&](Symbol_Ref sym, Value val)->void {
152 auto field = map_.find(sym.c_str());
153 if (field != map_.end())
154 field->second.config = val;
155 else
156 map_[sym.c_str()] = {nullptr, val};
157 });
158 }
159 }
160
export_curv(Value value,Program &,const Export_Params & params,Output_File & ofile)161 void export_curv(Value value,
162 Program&,
163 const Export_Params& params,
164 Output_File& ofile)
165 {
166 for (auto& i : params.map_) {
167 Param p(params, i);
168 p.unknown_parameter();
169 }
170 ofile.open();
171 ofile.ostream() << value << "\n";
172 }
describe_render_opts(std::ostream & out)173 void describe_render_opts(std::ostream& out)
174 {
175 Render_Opts::describe_opts(out, "");
176 }
177
parse_render_param(Param & p,Render_Opts & opts)178 bool parse_render_param(
179 Param& p,
180 Render_Opts& opts)
181 {
182 Value val = p.eval();
183 return opts.set_field(p.name_, val, p);
184 }
185
export_cpp(Value value,Program & prog,const Export_Params & params,Output_File & ofile)186 void export_cpp(Value value,
187 Program& prog,
188 const Export_Params& params,
189 Output_File& ofile)
190 {
191 for (auto& i : params.map_) {
192 Param p{params, i};
193 p.unknown_parameter();
194 }
195 Shape_Program shape(prog);
196 if (!shape.recognize(value, nullptr)) {
197 At_Program cx(prog);
198 throw Exception(cx, "not a shape");
199 }
200 ofile.open();
201 io::export_cpp(shape, ofile.ostream());
202 }
203
export_json(Value value,Program & prog,const Export_Params & params,Output_File & ofile)204 void export_json(Value value,
205 Program& prog,
206 const Export_Params& params,
207 Output_File& ofile)
208 {
209 for (auto& i : params.map_) {
210 Param p{params, i};
211 p.unknown_parameter();
212 }
213 ofile.open();
214 write_json_value(value, ofile.ostream());
215 ofile.ostream() << "\n";
216 }
217
export_gpu(Value value,Program & prog,const Export_Params & params,Output_File & ofile)218 void export_gpu(Value value,
219 Program& prog, const Export_Params& params, Output_File& ofile)
220 {
221 Render_Opts opts;
222 for (auto& i : params.map_) {
223 Param p{params, i};
224 if (!parse_render_param(p, opts))
225 p.unknown_parameter();
226 }
227
228 ofile.open();
229 At_Program cx(prog);
230 GPU_Program gprog(prog);
231 if (!gprog.recognize(value, opts))
232 throw Exception(cx, "not a shape");
233 gprog.write_curv(ofile.ostream());
234 }
235
export_jgpu(Value value,Program & prog,const Export_Params & params,Output_File & ofile)236 void export_jgpu(Value value,
237 Program& prog, const Export_Params& params, Output_File& ofile)
238 {
239 Render_Opts opts;
240 for (auto& i : params.map_) {
241 Param p{params, i};
242 if (!parse_render_param(p, opts))
243 p.unknown_parameter();
244 }
245
246 ofile.open();
247 At_Program cx(prog);
248 GPU_Program gprog(prog);
249 if (!gprog.recognize(value, opts))
250 throw Exception(cx, "not a shape");
251 gprog.write_json(ofile.ostream());
252 }
253
254 // wrapper that exports image sequences if requested
export_all_png(const Shape_Program & shape,io::Image_Export & ix,double animate,Output_File & ofile)255 void export_all_png(
256 const Shape_Program& shape,
257 io::Image_Export& ix,
258 double animate,
259 Output_File& ofile)
260 {
261 if (animate <= 0.0) {
262 // export single image
263 io::export_png(shape, ix, ofile);
264 return;
265 }
266
267 const char* ipath = ofile.path_.string().c_str();
268 const char* p = strchr(ipath, '*');
269 if (p == nullptr) {
270 throw Exception(At_System(shape.system()),
271 "'-O animate=' requires pathname in '-o pathname' to contain a '*'");
272 }
273 Range<const char*> prefix(ipath, p);
274 Range<const char*> suffix(p+1, strlen(p+1));
275
276 unsigned count = unsigned(animate / ix.fdur_ + 0.5);
277 if (count == 0) count = 1;
278 unsigned digs = ndigits(count);
279 double fstart = ix.fstart_;
280 for (unsigned i = 0; i < count; ++i) {
281 ix.fstart_ = fstart + i * ix.fdur_;
282 char num[12];
283 snprintf(num, sizeof(num), "%0*d", digs, i);
284 auto opath = stringify(prefix, num, suffix);
285 Output_File oofile{shape.system()};
286 oofile.set_path(opath->c_str());
287 io::export_png(shape, ix, oofile);
288 oofile.commit();
289 //std::cerr << ".";
290 //std::cerr.flush();
291 }
292 //std::cerr << "done\n";
293 }
294
describe_png_opts(std::ostream & out)295 void describe_png_opts(std::ostream& out)
296 {
297 out <<
298 "-v : verbose output logged to stderr\n"
299 "-O xsize=<image width in pixels>\n"
300 "-O ysize=<image height in pixels>\n"
301 "-O fstart=<animation frame start time, in seconds> (default 0)\n";
302 describe_render_opts(out);
303 out <<
304 "-O animate=<duration of animation> (exports an image sequence)\n";
305 }
306
export_png(Value value,Program & prog,const Export_Params & params,Output_File & ofile)307 void export_png(Value value,
308 Program& prog,
309 const Export_Params& params,
310 Output_File& ofile)
311 {
312 io::Image_Export ix;
313
314 int xsize = 0;
315 int ysize = 0;
316 double animate = 0.0;
317 for (auto& i : params.map_) {
318 Param p{params, i};
319 if (parse_render_param(p, ix)) {
320 ;
321 } else if (p.name_ == "xsize") {
322 xsize = p.to_int(1, INT_MAX);
323 } else if (p.name_ == "ysize") {
324 ysize = p.to_int(1, INT_MAX);
325 } else if (p.name_ == "fstart") {
326 ix.fstart_ = p.to_double();
327 } else if (p.name_ == "animate") {
328 animate = p.to_double();
329 } else {
330 p.unknown_parameter();
331 }
332 }
333
334 Shape_Program shape(prog);
335 At_Program cx(prog);
336 if (!shape.recognize(value, &ix))
337 throw Exception(cx, "not a shape");
338 if (shape.is_2d_) {
339 #if 0
340 if (shape.bbox_.infinite2())
341 throw Exception(cx, "can't export an infinite 2D shape to PNG");
342 if (shape.bbox_.empty2())
343 throw Exception(cx, "can't export an empty 2D shape to PNG");
344 #else
345 // bug #67
346 if (shape.bbox_.infinite2() || shape.bbox_.empty2()) {
347 shape.bbox_.xmin = -10.0;
348 shape.bbox_.ymin = -10.0;
349 shape.bbox_.xmax = +10.0;
350 shape.bbox_.ymax = +10.0;
351 }
352 #endif
353 double dx = shape.bbox_.xmax - shape.bbox_.xmin;
354 double dy = shape.bbox_.ymax - shape.bbox_.ymin;
355 if (!xsize && !ysize) {
356 if (dx > dy)
357 xsize = 500;
358 else
359 ysize = 500;
360 }
361 if (xsize && !ysize) {
362 ix.size.x = xsize;
363 ix.pixel_size = dx / double(xsize);
364 ix.size.y = (int) round(dy / dx * double(xsize));
365 if (ix.size.y == 0) ++ix.size.y;
366 } else if (!xsize && ysize) {
367 ix.size.y = ysize;
368 ix.pixel_size = dy / double(ysize);
369 ix.size.x = (int) round(dx / dy * double(ysize));
370 if (ix.size.x == 0) ++ix.size.x;
371 } else {
372 ix.size.x = xsize;
373 ix.size.y = ysize;
374 ix.pixel_size = std::min(dx / double(xsize), dy / double(ysize));
375 }
376 } else {
377 // 3D export to PNG is basically a screenshot.
378 // We ignore the bounding box.
379 if (!xsize && !ysize)
380 ix.size.x = ix.size.y = 500;
381 else if (!xsize)
382 ix.size.x = ix.size.y = ysize;
383 else if (!ysize)
384 ix.size.x = ix.size.y = xsize;
385 else {
386 ix.size.x = xsize;
387 ix.size.y = ysize;
388 }
389 }
390 ix.verbose_ = params.verbose_;
391 if (params.verbose_) {
392 std::cerr << ix.size.x<<"×"<<ix.size.y<<" pixels";
393 if (ix.aa_ > 1)
394 std::cerr << ", " << ix.aa_<<"× antialiasing";
395 if (ix.taa_ > 1)
396 std::cerr << ", " << ix.aa_<<"× temporal antialiasing";
397 std::cerr << std::endl;
398 }
399 export_all_png(shape, ix, animate, ofile);
400 }
401
describe_no_opts(std::ostream &)402 void describe_no_opts(std::ostream&) {}
403
404 std::map<std::string, Exporter> exporters = {
405 {"curv", {export_curv, "Curv expression", describe_no_opts}},
406 {"stl", {export_stl, "STL mesh file (3D shape only)", describe_mesh_opts}},
407 {"obj", {export_obj, "OBJ mesh file (3D shape only)", describe_mesh_opts}},
408 {"x3d", {export_x3d, "X3D colour mesh file (3D shape only)",
409 describe_colour_mesh_opts}},
410 {"gltf", {export_gltf, "GLTF file (3D shape only)", describe_mesh_opts}},
411 {"gpu", {export_gpu, "compiled GPU program, in Curv format (shape only)",
412 describe_render_opts}},
413 {"jgpu", {export_jgpu, "compiled GPU program, in JSON format (shape only)",
414 describe_render_opts}},
415 {"json", {export_json, "JSON expression", describe_no_opts}},
416 {"cpp", {export_cpp, "C++ source file (shape only)", describe_no_opts}},
417 {"png", {export_png, "PNG image file (shape only)", describe_png_opts}},
418 };
419
parse_viewer_config(const Export_Params & params,viewer::Viewer_Config & opts)420 void parse_viewer_config(
421 const Export_Params& params,
422 viewer::Viewer_Config& opts)
423 {
424 opts.verbose_ = params.verbose_;
425 for (auto& i : params.map_) {
426 Param p{params, i};
427 if (p.name_ == "lazy")
428 opts.lazy_ = p.to_bool();
429 else if (!parse_render_param(p, opts))
430 p.unknown_parameter();
431 }
432 }
describe_viewer_options(std::ostream & out,const char * prefix)433 void describe_viewer_options(std::ostream& out, const char* prefix)
434 {
435 Render_Opts::describe_opts(out, prefix);
436 out
437 << prefix <<
438 "-O lazy : Redraw only on user input. Disables animation & FPS counter.\n"
439 ;
440 }
441