Compare commits

34 Commits
Author SHA1 Message Date
josh fa77c0af29 Build an Image and initialize to background color; write to output file 2026-11-03 19:51:25 -05:00
josh 6f8e3714da Rename RenderParams -> Options 2026-11-03 19:34:20 -05:00
josh de2c51376a Reintroduce options block to scene file grammar; remove scene block 2026-11-03 19:25:58 -05:00
josh 0ba0ebfe71 Load scene render params 2026-11-02 22:28:18 -05:00
josh a3684b20b6 Add Parser::build_scene() 2026-11-02 22:12:34 -05:00
josh c1c8c46ac7 Add background RenderParams entry 2026-11-02 22:07:22 -05:00
josh 0a979c487c Invoke propane to parse input file 2026-11-02 22:05:01 -05:00
josh 88bfc29323 Add CLI option to set number of render threads 2026-11-02 21:47:42 -05:00
josh e964908e46 Add vector to common.h 2026-11-02 21:09:07 -05:00
josh bf09c094e1 Make RenderParams fields optional 2026-11-02 21:04:11 -05:00
josh 1475434c4c Add read_file 2026-11-02 20:19:20 -05:00
josh e13608b287 Add Parser and Scene classes 2026-11-02 18:43:39 -05:00
josh c8f6d36bdc Verify that output file can be written before rendering 2026-11-01 20:38:25 -05:00
josh 6b77b1a666 Build an ImageWriter 2026-11-01 20:20:31 -05:00
josh d7394027b1 default the output file name 2026-11-01 16:05:50 -05:00
josh 68d5b39cfd Error if no input file given 2026-11-01 15:43:30 -05:00
josh 0d36f13b17 Add RenderParams constructor 2026-11-01 15:39:57 -05:00
josh 88d482dcb3 Move input file / output file out of RenderParams 2026-11-01 15:26:46 -05:00
josh d71be68fae Add auto-included fart/common.h 2026-11-01 14:18:02 -05:00
josh 1947ea553c Rename Options to RenderParams 2026-11-01 13:20:59 -05:00
josh 7eaf5f9555 Add background grammar rule 2026-11-01 13:17:32 -05:00
josh d761433382 Add Number and Vector to grammar 2026-10-30 19:57:52 -04:00
josh e3d4c88f66 Add propane and initial skeleton grammar 2026-10-30 19:36:59 -04:00
josh 94105a883f Pass options to Engine; add Engine::run() 2026-10-30 19:06:22 -04:00
josh c05f2b1b59 Cli: validate that width and height arguments are numbers 2026-10-30 13:55:08 -04:00
josh cc041168a1 Parse command-line arguments 2026-10-30 13:50:11 -04:00
josh 05dfa4f977 Add Cli class 2026-10-30 10:26:56 -04:00
josh 8edec7e28a Add Engine 2026-10-30 10:25:57 -04:00
josh 0319619724 Move main.cpp 2026-10-29 08:38:30 -04:00
josh d764740b54 Make ImageWriter an abstract base class; add PngImageWriter 2026-10-29 08:37:33 -04:00
josh a233b746c2 Add ImageWriter 2026-10-28 21:28:28 -04:00
josh 917bfe4821 Use vector for pixels in Image 2026-10-28 21:16:22 -04:00
josh 6849d583ae Add Color and Image 2026-10-28 15:11:33 -04:00
josh 6d102b8de9 Start anew 2026-10-26 23:59:58 -04:00
34 changed files with 1486 additions and 88 deletions

No files matched your search

+6 -1
View File
@@ -1 +1,6 @@
/target/
*.bmp
*.png
/.propane*
/.rscons*
/build/
/fart
Generated
-7
View File
@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "fart"
version = "0.1.0"
-6
View File
@@ -1,6 +0,0 @@
[package]
name = "fart"
version = "0.1.0"
edition = "2024"
[dependencies]
+7
View File
@@ -0,0 +1,7 @@
.PHONY: all
all:
@./rscons
.PHONY: clean
clean:
@./rscons clean
+27
View File
@@ -0,0 +1,27 @@
configure do
check_cxx_compiler "clang++"
check_cfg package: "libpng"
end
class Propane < Builder
def run(options)
@sources += ["propane"] unless @sources.include?("propane")
if @command
finalize_command
else
command = %W[./propane -w --log #{@target}.log #{@sources.first} #{@target}]
standard_command("Propane <target>#{@target}<reset>", command, options)
end
end
end
env do |env|
env.add_builder(Propane)
env["CCFLAGS"] += %w[-Wall -O2 -std=c++23 -include fart/common.h]
env["CPPPATH"] += %W[src #{env.build_root}/src]
env.Propane("^/src/fart/propane.cpp", "src/fart.propane")
env.barrier
sources = glob("src/**/*.cpp") + ["#{env.build_root}/src/fart/propane.cpp"]
env.Program("fart", sources)
end
Vendored Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec ./rscons configure "$@"
+27
View File
@@ -0,0 +1,27 @@
" Vim syntax file
" Setup
if version >= 600
" Quit when a syntax file was already loaded
if exists("b:current_syntax")
finish
endif
endif
syn case match
syn keyword fartKeywords ambient ambient_occlusion color define diffuse exposure height jitter look_at material max_depth multisample ngon offset polygon position radius reflectance refraction rotate scale shininess size specular texture translate transparency union up vfov width
syn keyword fartObjects box camera cyl extrude intersect light options plane scene shape sphere subtract union
syn keyword fartControl for while if else elsif local
syn match fartNumber "\(^\|\W\)\@<=[+-]\=\(\d\+\)\=\.\=\d\+\([eE][+-]\=\d\+\)\="
syn match fartComment "#.*"
syn match fartIdentifier "\$[a-zA-Z_][a-zA-Z_0-9]*"
hi def link fartKeywords Operator
hi def link fartObjects Type
hi def link fartNumber Number
hi def link fartComment Comment
hi def link fartControl Special
hi def link fartIdentifier Identifier
let b:current_syntax = "fart"
+3
View File
@@ -0,0 +1,3 @@
if has("autocmd")
autocmd BufRead *.fart set syntax=fart
endif " has("autocmd")
-1
View File
@@ -1 +0,0 @@
target/debug/fart
Executable
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env ruby
# The MIT License (MIT)
#
# Copyright (c) 2010-2026 Josh Holtrop
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
script = File.join(File.dirname(__FILE__), ".propane-5.2.0-58c963344c9bc893689cbae5e4c49b1b.rb")
unless File.exist?(script)
if File.read(__FILE__, mode: "rb") =~ /^#==>(.*)/
require "zlib"
require "base64"
encoded_compressed = $1
unescaped_compressed = Base64.decode64(encoded_compressed)
inflated = Zlib::Inflate.inflate(unescaped_compressed)
File.open(script, "wb") do |fh|
fh.write(inflated)
end
else
raise "Could not decompress."
end
end
load script
if __FILE__ == $0
Propane::CLI.new.run(ARGV)
end
#==>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 truncated
Executable
+538
View File
@@ -0,0 +1,538 @@
#!/usr/bin/env ruby
# Copyright (c) 2013-2025 Josh Holtrop
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
BASE64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def base64_decode(s)
out = ""
v = 0
bits = 0
s.each_char do |c|
if cv = BASE64CHARS.index(c)
v = (v << 6) | cv
bits += 6
elsif c == "="
break
end
if bits >= 8
out += (v >> (bits - 8)).chr
v &= 0xFFFFFFFF >> (32 - (bits - 8))
bits -= 8
end
end
out
end
script = File.join(File.dirname(__FILE__), ".rscons-3.4.2-b1e276fdbe6b603498fe893d904fe368.rb")
unless File.exist?(script)
if File.read(__FILE__, mode: "rb") =~ /^#==>(.*)/m
require "zlib"
encoded_compressed = $1
compressed = base64_decode(encoded_compressed)
if ENV["rscons_dist_specs"]
require "digest/md5"
if Digest::MD5.hexdigest(compressed) != "b1e276fdbe6b603498fe893d904fe368"
raise "Hash mismatch when decompressing rscons executable"
end
end
inflated = Zlib::Inflate.inflate(compressed)
File.open(script, "wb") do |fh|
fh.write(inflated)
end
else
raise "Error expanding rscons executable"
end
end
load script
if __FILE__ == $0
Rscons::Cli.new.run(ARGV)
end
#==>
#eJztvWt7GzmOKPy9f0WlkulIji0nPTu776NO2k5spzt7nMuxnT6Zo3g0slSya6Lb
#Vkl2fBztbz8AeANIVkl2knl79zmZ6UTFCwiCIAiCIHj/3vaiLLbP8sl2NrlMisXZ
#9Q9F9h+LvMiSdJLNty/m81nqkgb5eVbO0x/G08FilCVHZX86KX9IEv39fFLm8JUk
#/VGvLJOnT5MyGw0pJUmODo4PTpJnSfox6zwepzp1kA2TqyKfZ418uplsjLOy7J1n
#TZ0L+dNufzqaFlBRtdbqzWajvN+b59NJC3J70KYqYuvkQ1utNclHOzZDgNN1d6Bh
#11w2Gdjf08Uc0U1jcBnI/qIossncoYn95E1mJdBuUJmvu9zKev0LgJ98GX9hudTq
#uJWX3d5O43he5JPzpshOklE+yUoAPG6VQJl5I/04STeTrSfRctRM9yqfX3TzySD7
#TC1izmaSf/EqUNuYl9wTdBD5svtQUPQ3UkcR9tE6BX3SrqrBRy9oDjsS5MZqQKfy
#5KkmV5n/nyzZSp6s2Xl/cAMsqgus4qO1Ooljv0Yn/ZRsVPq06ffKLBl7iVcX2SRp
#wxD8zFN9DqcZ/vOfn4x9VFT18yLLJj+vrP5TRfXrbDSaXv28qvqfK6qfjRYZx76i
#+r9UVB/DVJ3Mez+vqP6Xiur9695kjdb/taL61QVIytWk+7eqvk9HAz54sepPqgcO
#q/PBq6heNXBYnQ9eRfXKgYPqfPAqqlcNHFbng1dRvWrgsDofvIrqVQOH1fngVVSv
#G7hVbFM1akVWZvManonJmUA+iG/5tZ4YrBB/HJQnhL56XdRNjtfsCf+dT1tKKQEY
#BqzLnxX5ZW9ucEUNJqpLAIJH71/8tfvu8PnJy7dHr5Nn/5lsjwHRqy/j8rrcZng0
#Dt783klPDo5ep6fJMxjPz/OsGKfN5Mcfkz9ddYb5cJr0L3pFrw/px7Osn/dGp618
#0h8tBhm22irnvXlrOL+eMd3JIyiUms+vdyIdNr/Uv/g3/me1PuCfUOVTKt5zp4wR
#hN58XnSLrDfIiqQN2OaXWfeyV+S9ybx0BXr9PgwusEr7bJGPBt1BXsQyQ+3OKzDp
#zi+wsTJsu+wX+Wweq1TmI8Wrk2F+viiyWJnLrDibauLh+IKulM+B5qAMaJLt+mBg
#Js2Lhc22HYN0Gtyj4723b467L96/Otzv7r86goH+8iVJqZyZuFCwNchGGTBeWN6M
#667tNYB+P89HUAW5BdSVriQIlNW0754X08UMK3RO2Uirzs175adu9jnrL3AYu7ML
#WPkNl+zGMgMIxWLSKIgxiOqbBBJm5mTQnQHTjsvNpLyYXnUpeTPpjUbmZzbpnY1A
#GBkeMZ3cg0mfAYMDW0/6WScd9vIRyazxGKCWOEtMVzSSvDkcCy/JFPUbhKJ+0g92
#+oaFUQ/GSXnPh+8mY2pZIhWyoI9d6kba9/tq6tOs6g5681562kn9iukpm9qxbsXb
#q5n9yNQ0flD5mH60JtmVowYM4QJ4ejTtDbpserGKLczjnMA3VoiHj4ZrWsHXLGJr
#UZICpXIalncc6CKbL4pJ8jiAiTvFayqtB6kx7IFIbNbxtpzG+TBgpFY2nnEpCkTu
#A/xiirIDyH2ZF9PJOOOkBignAKVFoDopzJjeYjQXA4j5LKeloCrcstiYcdEeomiW
#TOrhpDeG3Z3K+uK3aUus0ab99TiQAPHhXUxGQJRk16YbduSkCXnX321mn2ElgYG5
#14jwOaypf2t8fPRlq7nNFYDIhAgqm/3yZsprlri8DnNZ92ZZA9vRW5JB6isyjyO+
#02htNB9sx0oP8hL/3UyMIMehAnwePNlMHvwkaoR4d3ilUyvpDFCi85YgtFSO0FJg
#0MXeRbuFEzdo2UlDndJpEw6+imZzNUkRyep+aCCnklCjEiU1ssgawEkA3EEhDFlY
#CgpXVs+imNjlE4YkG65qRgNJEm+Ni6wMwSoYrxm18ehMYeZBe51WdB+U8wEou5u0
#u99M0pcEMtG1ksb9mzx5lDxZbt+/8VtDE8my2U439V5jk6wQbqg16NZsAVOJFBZd
#tTufdsuG/vDVbE6s/ijrTQx9kSYBbXgmyNniPJsbac+lIaa77qvSRTaegpaqMhvq
#H4f8S+gr4ly2inF36GWzUSdQoO5l/fm0yDPbOKiT8+TmS28zOfuSnClz0tNnvyQ9
#RTaHnKl7XYGfzW/YXw7LSfZ5bqQtYmwxud6JFYcJ09jPixasUoSrK5JsJZ2HrYeb
#ycNW6+Fp01/skgSrFWOowCvBsPcXWTLJR7XLhuoP8VswxCCT5usPs1+whSnGIiiG
#rBg2nC7eDDmLZqvcC8QUB5zqbtRr1rr1lDnb4IBpAbgWaj2qt5hPsQwjvI9j9f6N
#96xGL7GLsy4DUzqqvRy4xM4pX+ou+UIAny0NqEKAxpuJi1HREa/jFm9HRNaKS5SQ
#kYDITib77QyV28aur6eeZef5hPXLjIiFC/KKq57E+GpTfFAUaFD5JcnEsAFvwlZS
#qp5Ut5eDuhkbznD7s+6WIKbl/BCgIpk5pPhiQi2PzGy+hcgl0F8ncQNZln0G2bAT
#lKPlJD1CWPnkPLl/owosU5xIfAOPf+4sxlWH/rtJ8YB2tm6cfN9P7msubahNynSG
#DK4UbtNRFIsxSxL+uXfPz2oNQeWBoTK6aiJ1R9zB4w+jzot9FLYkDSYh5UR+dO66
#PuhfrQErqdMsOoRNmMtUVgNlmM37F4223iNuJnwWs5qfsutILUjdpKZcDb+ro14J
#VDJ0PMXTWw2kRlQYGBpfq+93eVurNxL5MDJOHBSXm2q7H2j3tSzo9P4Y9kSBxgYI
#hXIz+fFsNO1/YtyHycBWRTmvMDfrPRmVK0d5P7vXeBzKFscXDiCaAO3W0jfWuQFo
#jTOQVzRH2omaKWac2knnlOFKuLvFDL+smSw2frRJMLAaQdcrJ55avFJRHeb5ZDKd
#wxIKv0a4e8DsfJL0IPUiK2RjCrk0INPqSbh+28lkagmF3c0n2SDS4jwrupfnODKj
#3vhs0CMeVam6suBUkaPWAuBWH+nS2YppcD1KQmMSDmyDZnIPK0evAoiQvXJv62HU
#CfE+ZfPdrWFV2/zqGSfpKBuCXaPZv/s79zV23uEodTqn0oTNT2Oc4luhBQWs5XVa
#pX9hmCgw/ekCbTZSYnDqqQIV5FtHVgmDs27umTVq4h99IIbHfW8YXxt7xBCUTyNk
#CT1u2cmHPuokZsV4GPjoRgAqQaw8Kn8zWP2X1VYjNTuZRkx6duDXU7954SvGHUeu
#cqTq+Pw6z0aDRDa8Ctc1bNRK3UJ7a9k2lCPr6ylpllF7Lf4UYgcTWoNMtQSrAq4d
#tiVft/tTJ4EhLGFuTObDRvqnrZ/+rUwVULU0L0lhlhCXnB8o07cn0zc3KEshAQtb
#d45KLBoWt+7fuOJLeTYN3aE82Dd8ymBNLc49WWIhITM+45Baixm6pngA/QNrSwSf
#DH9BMqT3b0wDy5RIobCposUq2WS2pPXr1m052PDNx8nvfFrHZ2ayA5P2YTxv+TBN
#2kmaLtucaHeaIAanxAkI0whLsNorItXQX02NRJXoqJ9loUlmOoEEuy0XhzorD0MY
#HNc7PYoSLh722YpkMXfHe95sCRoMZguItNEiCxZWyoM54x/IKHgdB8Zfcf0KZTbv
#soYaFQhIK3zE9ysuw9P3k0+T6dVEgcIz50RMTLM20EqEWGkRowVAs2bCrs8HlYeQ
#eluAuyLSIevP7bzSd7baaTi0vaANedgubL1K2JD/6apjbZzKgOnsLP4W3dqtpENG
#3AXjRa/M+8xIZ0nlfBYasX01iiNgGEgD8XKc0WFvQ8Fut/cPXj5/f3jSRR+Ek6P3
#eyev3r7p/v786NXzF4cHxwYEjUJIq3umPVHOTRG1soNMKO8F49s5bQDBPBw7uKkN
#ip4Dt0N+pHwryJJNPGvQfhgmQ6whUvNGMQkEnw2aQaXfYCRT6DOfZ8gRULjIhg31
#zyakzoseJmpF3gBVKUIvtCV9h2aNQN0eAPFTG0gHJqaIqc0PLsbkKJJNLtvkv02b
#Tdho4t+nDMVWvFcMjCQEsGKv7ILguJgOjOePPZVqC2CWcDNEvzsc9c7LBv3NNqf0
#rc5R73knqDoLpEU2GjUePAl348UlP9W9mhZ04HaM5elDnw2LNstP+cw7Q1RlY+dt
#mCMO2/CgFACwwYnAwz9oBoxuEBSTyW0q0IsYmS8qxSXuuNC/iDnJ8IxHjsP9Ni5g
#EEbZ6jZoQzEibkxo4zEKbWXIJiMARUTq0PmhXME0AYSZPuJeiFDwEBI3+BUNKdJ0
#CFP4e3S6Wn0j0IgaefVs9Yr+RbBjqsVet5nu7b08fP7rMeiVHQ2GqHUaL324v7q0
#pECMSurUW2GPE+D+jRF56d67dyC93x0dvHz1AZTbwLdAjXBHl3v15gBxecD6xmEj
#ZfRZ/jcgjoH0tT0mrMrrsphO598CLQNq7XGrqfFVY/fqzd5aA/fu+clvNaPmQT18
#9eLw1Zv/sRoyFKxjhhDs/qujtaCuwBfHczyZbvWvz69ycYEhMl5YRVA8MkJemaAt
#aGUwvSpjLa0PZaZ8Lb8vukjzrf/V2wyoa8A8PzZgHjxhDk110EbV0BxSa0ObVUMD
#Tl0bHHowF3NaRXca6ZbYLqwmaxWcR7eEs+bwZLEKavIEpSP2guIy0BCZonNPah1G
#l6lUhUi3i2XQR9BSfXGly8x6eUFLv9GTpRoDSqFQsfW5yPOi6F03aanGZZoncpEI
#tZWe7ashkq6uXJW2EtsfDhZjo2Ip1bd70SsvkmdWE55PuxdhAdqlkTGue3bd+LGN
#nklNYQkRlsVuOS+UNdgcCV2Pz6ajZrKj0tROOCGd2daLelvhnZJNA5J5UKEfAVlU
#DIakudk9NnOwilFh3Y2ZMguv74SMhaXHij6ONLsY/KeTdk1q6s0sZlnhWHEg8buY
#0JI94VyUwhBCDNYQuWzk4DOwt0CawNcDWI+rqfxtzBnV7fA2cjNrBZUpiVFY70Fd
#yXDXk5ttrISk0hgoLiNYhShAJZcEPEpi4FwRYRyDtJKOUoXNkwtCXmjdswO15/O8
#Cimx2x8P5L6akgFtq9Mc/3ZweCjWDMQkKIFLQ9pEC3xDg0DD4HiQosVzu0+Wzq2+
#gNOhgpsEr3It0VecsTHQMOfZuEu1Ij1/9bY1m8LANDobtm+bxl3ztKmOMKdMbE9b
#qKyskhnVRhp7YrH6sMIdHSh74+1MoiT3U306QabDtjxJOFWLoam7ki2M+czeZxrm
#o2yBzkCVF9lfoHEiK8qfNQhtaVOpBDp2w11ZetDEMuvNL4K5gtnCl6FczLLCqEXt
#tEkuGBUjJC43KfelML13VlZlldNF0c8iF6WgTmUeOe6Eydnk0iWyS1MwWSMN54Os
#mw2HsGrV3AADXp9ZTgwNl8y6j91z/i0dTQt7FciSwFyOgoTpaDHPaEwau9Lza1d3
#HQp7y4jOsL4Vu45OqEzon61xb0Z8rb4dG0eaVkVCEbpr3Ots25Rge4SBGZ5xz6BL
#m6WFrs3jFrpdTng0bWXuOg+f9owjSa8jxm7ZVKFbZbgSlI0NWAMLWONnblzQ/dNk
#axpvJn45oe8WMNOAgo0NjqcV1yzNCQ+W6ijNErvKhkiun7GxRyRVkQar1WQuUVii
#yM7zEp0NWJlGrBVWT9AahUKkeI3oxftzHqdrJ5eTi7xMtMl0vCjJwWV6mRVFPhhk
#kwSda0CMnNGgpREi55N5F6B39fk7rFao4rJzTrtmiKFUFc+UvJMAyCC8CozonKGn
#LlGDQ9wB0NwW4HlO/XAzllRqt7R61aCAWlH1N64iN1ChHUBYBlN0LTrueoS86uXz
#7nBaNPZUOp2v2I4qOno4NkPqoT4+6BWDO1PPoIU3AtUvM8esNAuEHio2u3xVUG5Z
#yo9zMcN7G6CkZjtuqu+6jqlqm8RIbopwJ2+u8+hDV634zC6mk2vtGLzjC2vVG8Jh
#/GmQF91Zw3jJoriyxXl5z+83gMg19XUYddendd0ubJhPaBWzAGPjc7th2DXexBpV
#mqJrD4N3rqhdAA2foq+eswao9ZCnrXUYqUQG15S8Fd0IFSoPa4T5Ns6X+lN4Iu6K
#SkAq8e0X0gdY/NMvQtBZGec2KZZGmLAb7hzLx4Mm9Mau7IBxIt2NdOQWBITVOnma
#/Ab7/koadtV6m036OY5zdBlFBbMhsRfVDAlEYoUCEShynFRPn5qBtW2jGq/TWlYr
#8w6lqgrBEnrGWIi35PTFEnTMcQ/IMjln4GBuoGLReKz2QEVWkrd0w0wRDbdsOhVC
#lUke2TztzXfz5exLcu9Mm3jM0bjZG8CvHqzD0GN/wfAPqfFAzy6mIDtxudcXJqAj
#OWzeKJPtcaEQJSHZsU/k1O4uVLheuE6gmuU42jgsM631UdKIcEDHjQt6EqN5rCdg
#tnqj0Y6625AJ56R7FX1wdz+hAu6RWR09RLUlBN/JonUmAOUjp+nGtqKSXkChjaCQ
#zgyo5zbPQ5dD/r6/iIBQLs9z/o7aNE2Eh/qliNxK+BzwekqKmEHmsTK6VoxH3H16
#D/1o6EiVmKuHSig5aplWW8m7aVnmmNLPi/5i1CsSyznX5ORz/0ZzJxCr8+RU85y5
#h7PeZlyFlNseD/6SRnfoNu0f5XSSrghK8vvB0fGrt2/Q//DPrX9p/ZTWSNo9u7l9
#99vbN3/tqoMsrNqmBO0sV7XR965BAd+aFBJxyp7q4ifEZEMQVMQlhI4xKpwDEkYX
#Jt3nH9N80ogExrNXHjeTtKUDMiCANOYOIzxoqFjc4YY7zjgvMlZDWeojOkbl9cxh
#w/WruQ4VEFCXXLzLxbhLlY19anc0nX4C7dTksp2C0HfsPSQ7z7QSSpep1GUzoVgy
#FL1Op7AVK4HeZJPSzMeHhyx0rjoMxlWq1qXhhZN6wwu6G9349+O3b1p4gNFQ4Jt1
#yiVXw/XEZzsBlNwYXeUyvinQiwXJdzRh3JBwj22aUeya9U2rhKqxmlt+K2wwvuBT
#9AH+0WZW+92wgFzhuv1CpHgSu0kYLSc3k4PsbHFOiyIP3rP3fO+3g+7+wYv3v6b+
#iYHyi6Vp305OVPftnURQJbNBCexmhDLs4/u9BfoM4qUBWJ1REBN6yXSCl6E/1fs3
#c7/DH+LF+G/joKy5Vg9hetq66JVI6Z2GJTqnyddQRNCDMettiIO3hoBESB3FJQO1
#OIGIgOVnTKKOFqJ8XnMhIUaqCHFuzVIBNTuWiOgsqqWQCujliaY/GgsCG8BnNsEg
#Y5NzILNmQlD6cUE7y6g8VJwnaKP+p/CmpKYSbIqY+6QytNuv9//Susg+Kw3CmFbM
#EekfjI9tJA34Bk1tahhZ0V6T/SuYWE2Prhbs9cTEQqnW+GMAukNc9mwMKZWo1gnY
#BfS/JIN+J6UyAMUFxpHGuCLvz6niaThzFJbPIm1+r9nwFQPH+6LxQZF0nl9mE6V2
#QyF0pJ4OE7G3tnIdBBWslFhsVmSX+XRRxsp//aSSWZrWDePaTXb4ZMscBDRC4jcj
#N8X/IKMAy0AP/gO8vzHNuCAypxUUKemSTmFtGpPY8YnDa9dPP1uyag7aAsFEtDmr
#Z6MefoZWCCWcdH8ESannBmK5ZYNBia0n6HLlN5KbfBokjwIKMS0XPpVq+iUq0Lp6
#zOvXfVZQD9l3UwDuOAIenTE2RIDzsor4Xz0Hb9EDkMGLGa6kWICiVqwik2soagj3
#jOq33lStuz26zd5IfVv+Yd4r3nrrbGan6I8fVWZjGqcMtOdfBev3Rmj4AewsB4dA
#+Pj5G8uI3bBik8nZobWY5P/hDCuBNJW7RI0RHeixdqzOiK5066mMm6K6mchY3xsH
#UZBEORaq63UicKN6ajLpipt+rmg+IkaasoaMTzgQCLoFB2E1wrVNLGzNP2IX9EU5
#gnTvnmV7c3/Ola2zxZsgRbqWszfRJwaYvCcDH4X75TIboWOndY2Nz+2OxZcWAQkU
#+Rso3PixTebS0CBnDjdxasQlzaxXzJVL8PxC+w9tdz5+/Lh9ui2KKGdedyeJ3Uai
#WEPKe29ednIVddrJgnJxhsChDWdm3FBlH28m6mJFYJThNhYNIH4Wa3oYKWRIzmIw
#wYTXBU9pjILRj6wkUddgFtbp1iwg8AnYYNDVtdh5TNcCvTUH1AXICnkycp4QAVYT
#eSreRw3WK20g+1E3YiJZBAHCaOjVi5Io6lnkHyWsDB5Q2YBEMgiuLVPDA75tmblf
#kfWVAhgTH6P/mrD8mmhX1vlEGC70ISGe2LpuoJt3VhQtodVc9Qo8smyDKuPALyEZ
#NKppUSxm8xaGqZtjdLz2x8n9G9NAEHrDon4TuqwEbEKnEi7WUGzI40VinuBeyfUM
#7/4KwG3+pGpGNSJeStDRB3f/hgjZXEsVDFDuYGVStiM6j8NBnsjUFFy3S30kVlw7
#cWyozg7G0wF0Oy3OUseMVmQL0kQwM9SB1oDvyuVXUQl2ktwFoN6jYY95HQnPUH66
#Kd1O/RoseP+8N1/EvEyVb5fwTPXyeDAz6U17YT2jPT8Lq/lrVG/pYbUbHCybDIeu
#8PuxqadewSCmXODNFor9xcQKhUvjE8Mb3kk67PM0aTtLhAbq1zJYsJrWmY7V1lnm
#HvmGaX8z2dArgHF707HGga4bznNpg7fuQvzTIGGQDPrRIkvQ1Okr1GZjg7UdPzyr
#cwa3aXhs9+dVZ82Gu21A0RgHyfCiu3OY1yp08gyaLYQv4+7VtPikX4xI79/UHeou
#t12AEiMAQu3KAmy66XxOq42JIg7N2ELLbQUSlsDzVFSwWiCdZkogeKJ5Zu85rA4U
#uuIlh7ol51mw8kZIFH3cA//k5HL8D3TI1Z3XgV15KtNDY5e1UjPYaC9J7eUtDoHd
#4Gq1WiIMtre5FlGxBWQND+q7pT7m76LeDHCMcBqEa4oGaroD8SrIcZzN5yp8KF3M
#AISJKvRElG62Rf4AVZHB6/V1Fay2XJDwlzx8oSLZBmzqouGtw4qro7wgm6j216mp
#yE+1KsZG34fhwCriourA1FZUNDb6/b4hguEdNSDpHpbFgcCj0L3EVKHxMFXiztMA
#ky6dRLVXVwWLad+mrUgsDQ3Ifxij34eqf7rqnMMPEJaT89AOR0WwrnVw6/fZZhqk
#JSdBv1/pdquRcKqxL2lhMLTayi9C1BR9+tTBYwhDIdoWAeY7yWNY9p5YnHSQFbaH
#Js4xPuOglvWdnQLFoMtJyQ9rusBI+zTy2QDVNCAM7bwxZFkTNNuIf7jmk8+f78Ap
#jx798XgFcUJeefTobtzCCfGV/PL58y04hgr/l+KZAeeYwVocs38rfhmsxy+Dlfwy
#CPllYPgFfowG/Z/wr5BhqBRWtwwz8BmGUWGwgl0GtewyWJ9ZBopVBjWsMrgDqwxu
#yyqDtcXL8Dx6d4Hsh/1PKiip3ZnoJGeWi+tSnLEMlIdMq1JJTHV4KJhORXwAXelc
#Bc9zzaukdZt/KBQ5rFndpB0nrUPpCrYlh006+3S+pZZ/p8TpWmQRQI7U+x90um3o
#qtwUWhGFj362E1cXwz7q6suHiR3qWj2LImyK/STtttD+AbNqa6uvok5sbY3yM7sJ
#c/vcjiWVvu9gxtwLY2zI3d1M1M4dA0uBskaPv5C81jdjvLtT+dCW5zFwy/m0QBtK
#gXqhBl05Y+1cUpC8kjFd64IMAg31T7c6Ir0uP5tpE3kH1M5T9K93Nzh5CeNb/8O6
#s2EvUShw3hRIVfCn26R5G7vhORK71U/1dq3KAxWk0tbB25eMB+9r/3vcKjIURBDP
#fIIuLiBH8AewA4hRfKsy2cCPS+i4d25k3mv7maW6sx/XvhtKfc/VgUkP9+koKH1w
#s7e3TJ3cS7vHb98f7R0cq+wqMrDyJ8+Pfj04qSk+FcX3D969fHV4UFN+PLQVlnYa
#aN5/EuMgt1/TCTz8NBqI/AiPYn3BBVJVY+SEei5OFAVaUYfTumT80oGb3nhCKOLz
#NYjQe6/3l6mKBuggdG8/uyvnd0VvzMTXAS5ubM9wEGTHloFA8HuZJGdF1vsUocBX
#iA7QOv8wwgM06O8kPkDH/e8oQD58uLUEQUr8PxliC64vQz58+H9CpHI/lo9n02Le
#MD/q5Icp010hRUS5W8uS/UTV5pLEYXdHMTK4vRDRWNy/MY0v+dS34uP7yon9Wysa
#gz+KkBBcwDwaefJtBYao7IuN/e6r1+/eHp10feEhat1JhOz/k9SQqv4FckT21UkT
#2dX/f2QK7N4a8F+dJIHsFTJEl7i19IB6Ra+45sKDcKmQGxS0PWYrcds/usnc1Qfr
#VXPOFneCiFWskj010qdGwsRljHRl48DYmPLToHU61v+uHVtT8/qK/tpfdKtdb9x0
#EsbGT0CpSvosvBhagRe9c+ZuHeUQU4z7QlMTNUW9693rzLzKlWHEHBVVDFBM7QCn
#n1atFnzUbrNAZJ+zdSW+nrOMmirhtlJeV/PluwnpyyS7LnknmX64/8+R6WFvAmlu
#eubkuOnYt5HgtZYtjgXjo7Dlu68I2nDXsAa8qCf7aoOttnTewnqqV5l1rJ+2U1Sn
#whAe2L9plMJ+17FNvPvyDVhz4kxXtFPtzUTee9p7XNnQ0YTOTK6GbMoDkHHxW/Qt
#afV7M5Cj2Z8bGy5iz3yQq7NkGZIqnzBPW4nMwed8riEjQuoXekjmHBVTRw2oQqwm
#F9B1uZ14P9zzJ+SFdlAUk2m7ffDm7cGbE1c3RU0f/nvy07/VvUGoeF8NKgqA+KCo
#UloMqw8k3TSInOYK2hCp0l3RxiENwxzzSJQROGGQg7oe2TdBvmmXTKzWO/UpH0Zh
#Ub+iQZwXWWUY5yo4OqyzDHifrVUzqFjrH8JPA+i44htSWUew9aIVRQo8fVoRZrte
#MAfHd8K1T2aR4p2qMx1dmR/v8Zo8XVVzx0H1C1A0OLX25AFtxENpKdx4xEWkDG/K
#4kubp8GQ6wnBX56gNS5W+TSmSEilWb0EhV2uuBblohsQYeTFJ0Ez8YCDfwnq3orO
#SUVnhG+AC+R2cEUcwBLWvs5Go+lVLc0JAJGcj2U1vacTauS04mla6WfWCKoJ97Qa
#uO+KaZ9DDcv1e9E4xTrMsqUGg1FxvrnHfasS5S74M67iyqMa6TXPJnjxD5Rk4Yy4
#XMu1zL+5sIaX0Xc4WMMN13ChNl74DlXcdIWvmWwkP61vwHKK9p/+Vwe9QZbJVj/Z
#IlpFLE7xZOtJQcseCtN0b4+mKsDTWXfQzWtVX7X8098RPZtHj4xd7FrH9ee7HG/M
#M5DywE1P59cAGR1LT35h2Sc0xid/vBH+/Dkyxh8+/OEHud5d5/uZnvuAfmsMGwzf
#8Izj+/V25077HLvTRlemU+6tRLR2aOLLp4lOtYlXFyAOEQBrzuOSwZ24RLzw7Phk
#X8mCQX/poYDYsxrw2Z2e/aNcDIf5Z6h59P7FX7vvDp+fvHx79JpejxnDWnX1ZVxe
#l9sYOL8FpSl0fmuartGXYRTr+zei3eUaXVvDFML6L4baEWPTT29vTZUtBzBNvdzj
#3+rzD/fra9fni4eZVKHDrcNIKfYqlCk28ovt7x+8+/XgjdLU0i28L/zswY05nlim
#p5tRLnd+5jWj/kU9+LQtWiRCd5BE717pd3rojkC6tVVko6nyVd/Cy1CjZ7O8n1ap
#YhbS2xf/fvz+JXaS4Aj2iN0d/daGp3VkX+I9EbbGxie+ifEfSkFkZPAFd1lQxGEu
#1fJVFWAgeCNF5PBbmQFo/MOehKnSnOsc9lc43qtnaeSOeK0qHd1tr278ypB/AQig
#6XfEVgg3xubAsF3zoBOrvwObTBB86Ho3BK5HSte/wGoFUfr8SM3dXqFnLqTsvVa2
#aYD34Ob50TLBv2kq4U9taKaf2iy9PLV11YzTtYt+aXOOQ48rSJRNHZtlRrTy/FjJ
#EPxtX7hbYro6gldl9Fth6sMhG2CITbbVvVWv5W/YrH79S2CAyUyyLunTABLSVGVx
#nJkQxUF+/Xo/2Xr9MmGC1JV1I9BxieYlNTMGBu2UlbCGa1WAUHP5WgZqFFrHBrZW
#7dPzvnGMS8nVyxEXBjw2rHt7cfruOfLuMeruuV971cNLTQfDSxh8h+a/7TAbjNYZ
#ZvMEnRjmaJpnLJHpYvXeT12emMZECVt6aT4R6JLBs/zDG+kKVsA7+e+O3gKdjrs0
#KAeJTDRj4o/WGiMU8oLHsn2boXcq6fmjR6lLk0z74UOcaz98uB3ffPiwHuNS+yHn
#EhrfB4dvzL0GqbXYV2PkscuHD/6Q9ZNWfzZLcMeZtPZMwX0jcwZW5giF2CWKMd1X
#ggh/QGFvLduPk1S4Z1C56jHcjwofbPVbNflth2x//QHT32x4UudAlO5HBnP/1fHz
#49dy+oMqvf/+9Tvqk8quISYrYDHcGuRlryyzMd5b2NpSJ+G2xrs9bA73kd6Y/wb/
#H3Lab023vmacdUPnazQ0LK8n897nrelkdP01TXoTY2AzAvchOwLuNWA1LV6Z4dKu
#B5N8ZBMik4dO1V3/DvcVA4Zs/G2Z8jDGTIcHuhPDUfY5dYmSvSCBwEO6xsiXnAFd
#bVmvvcgWOHV5wcZ3xDLDgXDH8TyFT6WerX/4+2ujKtNFRpnh9ZfSosuULl7NUA5e
#ILAs2BUwv/G4M+gesVSOINjIUlzLFJUOG3MMgM7yvHkzNUBB4fiV5YnNVEquMmhJ
#MoCOfzNqrti/YLIYEUz4FsouwOG/a/QGjUIwiBqT74QGmpC+5eBb4FxnVw2gS6Hd
#9rrifmld2BUwOp7w0FcZ/pB9I1WPIK0/bHGFz6Dz/XD5DmPHtTmjtvoDsm8n0D6f
#QExvM0oSz/SGympwuuIdFSqoXTc0cT1Otf4tm/72I7HvjQNvy9pDeXlrI8Uq4TQL
#NAVhOzbqQMoyvQFTWoOu5ZGssvuQUUkAyqsjwaFPA9vIVnnRK7IBK1qzynvt+Os8
#ZbMsuX7Q2jHKz0Tp6tVmgJEp8dyitMrX8av/rX338Y0enioJDAlEFfi3hqNNrlxX
#//rcLGpnOT6Ow5KlnoEpCJhy1tWsXGGrvSvSL1cHUKKASe4LHyi+mhaDyIO7KooS
#O3ZJngYnMYRP7B0eLzgYvmCUT84xnJWoasLJAIAuxmayFuJdVrLsnl2b0EDMiCyK
#8FBFLMqADn+YO2P2bj4gi/JjkfLoGXs2ajf3TfwyCnFDPJ8o0QAqmEhn+Ccfklut
#F8Eo2rmOKXiqfHFt+Zivqn4dSBNlwj1ocu8p46rmqCnXiDwWEFVqMDH/xqLHReK5
#DbozEPzqmJjfwYg+P5z1L6YVwLvFdDqP1Zl0VTSySNuWKF4cMPMsoHzWMLcPvKkH
#BQUX0fvbkzk6MCHjxV556vXn+WXW1SVLP36vTnfHxzqh07bPOxFxvUv7+Eg8YKtf
#84r7FZ31ShtFjAqHT5/t6uyOLbuZbPBOnWrP2S3uq+oov04YtPs3u+I2qWAw6Adv
#zj6WxjiwPsRCbyIE01U+v0gUyRNLcvMor34OgmZF1A3J71tdx4KZ6Pwa3cjgaPsR
#0eTh2ToRrn6QpEjfqeBjtl+uLHwNpyDO+8DrFPZLksd/7s28Phm84K2mjoh8Z2Re
#NuhG36h0JWsetWbPc8LyI2S1pryOhSoA8mcbXCp7LtBL7F5Mp58oHYQMBj3cTGbT
#co6/vLFwL3Z/phvQ5gVI9iCKOeN78f7V4f7B0bG7riEeGqXJw2/0+M+hmicpW8hS
#8y4GDQ4BNCsBgEyLPwpo4leqYvdvQqDiHndvMDCdlO3XRyLZRRnMDp75sTNm8XPn
#igzlvBiZUpdZcTYVi46ILqrrinx69TeC8K4V+/HZK1cFyy/0UmniVIHdyuWJMxtb
#FKxCgGnsmN+9WYt/uONKRA/S64yT31i5g0mntwDhwutVxOubVKx06ywpdavJLSRh
#ECq1NVjMdB5hyP2lKYEcpdvOfdWUAuHSaZOgPxvhC7tW5TA/lSw4pfvmqs6zKjhe
#oQn8CEupZ3nwu2mLx9zvVYaOz22xasqJUyF05AQiJXXX6kAYFv7KBG6l8dhJlArH
#47vaMqCGyis5fqNt8mpxg3gp1R16P9lAY6yiO25efnU99JbSrChDiakUDf3FhSZq
#vhEJVS+bqJJyW99FlxF8JSGCoeWSVb1QLBPoO5SKoauyU69HlKUeka7sDJVp/OiX
#r9VCTIuwdt2ySaxy63YlRY2C0jV0wxW70XYSdpNJ228r8lhw5btJvMRum7jsq1z6
#QBKiyjid8G6IV8SFNMS1OJp9PT6bjgJhKYsKJ6wKHUH/Q3MvfNH6OMdLKy9MtGs1
#l7wOiPUyikckBj+bsh2z/QwfcY9S1HBZ+CA8nzb0ZHj0Ffc4z9ZAwylRDQ73aKo8
#PX5ir2ArWimvwk3ZNzb02ed50TNBOyTVXJabP7x0CrsdlxA8r+xukZv4xuyzdV4u
#zhrpx4/oAL2dRhFqAa/vJD/+KCvio2fzLm59dsip2u1hlttsn4NXhFi1Jd4YIvfb
#6M7MBzQVPYN+0iXRce9TBhvrUY82uRQ5v7qZZsV7027QZn2zucjRuDIQISh8jGb9
#GixsZd0mb2lWTFm4YLkfa9W+Xqxr0i0Z2G/JDYzJJJ5EMXXVy1E+cddOW8jE1qZC
#WFi9OR8HqHkQi8rtjleijEFxKi7XcKXiK3ZloM0OFv0MNDtab6Cbi5F7nh2LuCVH
#ZSaPeG6LwYZRwdgF0MFgKsgbvFcXeHFI4mGMAShwzZ5UJq6gZsycYKogeiwZ4xbE
#G0e3dpmlNGQV7lRlG+R42ircYmW9cYw99ehhGZh0fAdr8wKKz7qrYfMHwdfj3qAK
#R4UvziWPXO5NQP72dxDNc2U3xuU58nNQTJmx/Ct6vi0mCHwf4yBs4tEzDJP+Hvbj
#f9cyyLyXAJTQTyZ0QfglWy//jtfgLvPsih5OVC3Y+xJ0Za6YjuXLo+rd2eksU0ah
#WIj5KrsYIOdvyOT+D6WbNrJ4q74/6rzeOJtfTAfdcV6WMNwN9akji/LgMFbnt9c6
#VVHSe9g1IkJgUy9jpQoqAeBgpOZ622jL0q1gz21ccwPdqGahi1EmsLTfe8WxfAZP
#m1DeT7LP+E4PDQQMfrEg7Skxqi4+b4mX9RWs4E0fPgr2oUCAiFcO/Kf3dA8TE4JD
#fzsDsErgSrxKsdpBSzeBdxdUgurdC39vlBgcVDGh4reGsDjOMydv3bMnTuljQ3VK
#zMRAKyTaZuQCdGEfaQaT5fXVWzdymm0KAXHZpi0AT0Syt704I4F8DsSXtCyibkjL
#EEut2WsZqBgM29bNCq+UXi/xjMiwoC6oxyTU1l/0iiLnoyTtnB1T3z5dy7Ruf7Bi
#ahoZcGsmvLLSmucDYYq5Z1PdtFWZVXxlWVwXU/+EEppZaeUbuMjmi4GIcLAYVHPx
#Ak9coIBox1tLFKMvBr6sq3+5U85Ru7Ww2HbMEIiQAqIz95Ib7EsMOmCzDKrYbTsn
#QN1r9gAFbV0hUs5MsRhwckXQBwZexBdeq3wRE/aGaLM0rOFxBueisDjGky4yOj4u
#Yb223OTs9/KpVZ0t6thCdZDu9kZrfKRXHWOI8feDxAskHVY8mUtxCnzusqrZXUKG
#7ohq3iTwtTuvajUleMGmYJ/1KAIsxUHUMJe308KtBBM//HDIoGFl361mrbdluQRx
#m4WmRrFbcXadkm3toUjHiV9GGqvM0BMJti/RPuAfdae4dt7Fz3B6iRsErfzN5VPK
#nhbCaWyfRGZoNdhvSzBxLgfjGSF1UM3foAfv0a4hdb23MQNxVd0j9861eO/Z6I3O
#lKPUx6hBZ33Mpov5bDG3tpm4EWmF+Wi3y6YTQ9+IxVCwmI1NfUUn/DmWTM1Ei30X
#WBl2IF1p3WzpONesniOgVLJWYY+HCQxMwN13lq7rC6HIKMYoEqDJ+UsJFFWZG5iq
#nj+VMfOY9U+/b4GGuWgEJsoxGpAM7ZdUYlEZTEe9JLw4a/ypuPnbR/hfY+dZBx8U
#xrB3da4KTVaL12HWtGaF9TEm1LU8hn/sOsb7on8DZxXZUJUKZcmsyCdzq5sXi4mJ
#omN4dzOhM17gA/UGHHTIvvJuz9gw0gI/GVMhDvzD43xoKlbFyXJRmvytfBneIVeD
#LiGGIX0cyMhZtgq3I3u3BqSgToRjFAnk+XgNhCSGSjCv86GB4ZSv6bw36sasiOpT
#HnTML/KSFUZMiAOGjfRPsMT5sMjqssRIIGZ75DLZ7MKAhBkdnKQdgCLbQBN1AHh5
#6nbxschOG3r8R9MCMOiOe8WnxQwt2qYtDFVh4j2lTflWXmyuGA4fTou7r5hyx1i1
#ap6pnWZMPareDYuQBaC+zJjjg7VVGN1FhUzRC5bOVIFQbr6UXxJQimHS6GOI1iC1
#u6GB70JBq5WUE6nvxu1iUjpPDFPfbs0kZKf9RWNme4D0Ei+OHKKBrwMElC7M02xZ
#xT+jrDfpCh8SJeXl92YSba7a+aQj4sQ/q0CCbfIUR3Qtq+lF4jybdPkKp4vTqGh7
#RUPWtQHx9KBbQvvWK3qyXnCIzxTRosrj2DgrEhFxbFhyYNTi1WWcfRe9lKXjDIQh
#UVBk3O/SqUOswcrxR0G33hA5CH07BCRLBDwUVKxdvRC39Dq8bA1y9po4b7dap2Ra
#ZVjeqZIGLREyEpngnZMBpoyhkdg6xrDBydGvMd1QA8YqJblsM/ExquJlIbv8YHFV
#dj+99zZOPPU656bcL8V3Q8iDbHmR8poWOutOLvY3oO5UeLjc1aoYrpPEFtVIsmBl
#qEXpczy7KUZXi4aLz0laBalNtkVVw0kCOgmaZ2Ou4sZPMI3NM25mWV0LOMw0xmpR
#NyLphPqJez7ctqLP7DqmTszwqqvveSqcwLLqJDeOJsF7ETQTBOOxJxU4w84noI0M
#nHMnbemW1oavF2ZsCU8tTKuRk4uqEznCSoYgCs+6oUPpS3V8NZ9qM4VDSU3MpX0P
#mUCKgEmmJJ9XbCVxqZyFvMNBaxpQ9gBWaZMuUJGzL50nJCxP+rhR7wQS2k0vYjSR
#BLvr7PwKjy5jqnW5FMUz9IyrGlp34l1k4+llRmzqVxfsdwfWo5DAyHtaKvicV2Eb
#DNAyrGtV36gowL1FLzD1bdJ0K4W1TxW3C54PnviUimgOCAt4UVJV6fDsKY6nhhoZ
#q0rPjcjZ00oTq+/PoCQbep6gEgR/5eUFLHIqucGnAp79UKpyLMIT6YbnafEscT7V
#3OVCeLmww6LatknENEMZU+cyI7ahcynIYzykijTZpCXomVUJmOkXlUJDFq+aiQ1t
#vQFQ7zNrhvrXKXiWZnrEfWD5MISnxceeb1+I9yqoziuEsG1cPD28Mma3lWRVFYUA
#WukkQW8qB5B8u4cY8BWLSjBmwfKi/ngW92qHE8ZCkr+fMvZey20ncAhh7LGybngB
#y3CUoRoUO40YjURHq71iVvgPaWnN9c/KqhbXqiNWFDn5SJtX8K6Rmx/Ggwwfp+Eu
#ZHpebCbTs380AycyJ/oi89IdkEfHNOq6xadf6O9AZTuPT1tSScY9BlGgEe9m5IpO
#Sweu197n2tGeZzbDqXcHJeLurugW2XX0CP+MUX5FeCiAsdbM4wNUuRCsiDccXWQm
#04l/scUkhQKbbh3aYL4qlVPunhagvVF+me3EeshVJzI9IOqcgzdEY2taC+PLEiOi
#WYOqlpGgXI0TScW6JlFUf3N3fHZbO4vE3lRe7cdkVSY4KuHX0fSsN9ovjdO+dwtX
#WaFZP1QCWg0dJD4AbsvvbmUmt7u6Sft5QKuxMUMvqYJHfVXGqGimBDCYXk1GUxiy
#RQH7kEFWVjwOye73gJik+BfdUT7O9cHYX/zdRlA6E4GRgSUopHb2GTZGOw1st4ke
#4u5BhIveT3/513Ix5rWo3n5+DqXb7ePfnv/Uusg+D+i7QeDO8gnxH8Fr4oWoVQAj
#q3GdJBH7qiJHu93RK6Qd09zKrFuWI/QGKkbSy/1iPp+V7W3u3g6ryAzwy8SNEDRZ
#gh4Bw1bAnHIHIanTI1SvIU2Qg5skpKdykrzJoNRvJyfvFE6AdN66mOJ44y/18KbG
#vW1+qIUQ0ZZy2YVDVxxT9UCc10MEhAsXtY1eGRj2GH6rqniJ2K9M8dPVcQdmN4Ns
#TQdYhTHfy848Ra56XPVzQ8dEdvJLxcgUXPesu9BtZxEqFPdvkCZL3FdmLXfqUrU3
#M+TREtGO0pGeN+x+jK5RORG96NCVOBvQCdUDDFBVHijbR6QnKW8/NPmoldXKEdOf
#TmoiaqennmDRqoZEv13dry18osvJEW0XWceIoIVRFY2P1dszoa/t+sNcPbARyVMh
#4ZJ7zzQfO3m2GiXuDqzvfSTTSS1eUvRnk8v4ndroOqQfhTaX9h10mNdebPsqqPGL
#1PNe+Uk7n2MogdlFr8y4lSJryZsv3mhHuzbr4Ut0ymmENpGboP5/ykq8D0rMaA6N
#HXIngEa7/Q4rUh9uUdlve35Bb0lMJyL6yMGb3zupetuwVUL31ZIFTUJS9/jg3fOj
#5ydvj2pgguJM10VDxw665NjRC6rnn0FB0iRKNW3oG6kVTXhwAHJVozVNICC8mivb
#QHVXJ1b5O5h8XCpNUbogUUdHyS1sDBiUKK4FcWpDHXmKGwOJzjPIBCR48FjSLlYm
#6D2VUCJuWlz77jt8w94hbLgaA9XwYFVc8JNymtVOt4bpZsL6FUIyuNARZvXbm54S
#l4LowdApijpi1ZAE8wpGYTOEWWVkZt/IoJxyFOL4NwjTtJWiqNVfAe3386I1u2K7
#DnqgJVXttEGageKFIt6ORvJQYfwwVQ6yukHfFADK/nU5z8bs6cX+BZRrI6Sq1g6z
#3uWtGnNrGrS5epl4Ka7utJPUkdC9L7l6sSgvbK+CLYGMa8XcjMgDld10Ydi6qtaG
#p0w3W0/izAbdmueTBQuyYOMUmCzfcqnA4q79Gd59AI7QiWg/qRAyju9c2SrmXysq
#iI/KLyxSl+WGNRy4IqoXVY2gWa0P+XuCgF+DqAsZJQQql9GX51CrVwyiGjPbt6Aq
#fNbrf5oXvX7W+enU1459eqnxFJ31HJ/oyVDUFDdNO+x5WHFhrvKYMriEpuCs1ivl
#zED9pUrjoXGtLiAB6chPWvOIb8JjLBerVttCN5vgfbHBTsMkSNeR6kbqa8YbOy+m
#i9ktFM016smGcHNtI5U11mhBdcJWqStaB9th0bEg2n32aHe7fzGeBt/dI5ECWwr/
#W5aYiY/uqJDf/JNOO0YjljKaiI9u6X2yG2zt8SdYZvzvLm9+fMk+YAVlX8VYfHSH
#8rPwPmW2bHc+XfQvzDczFauLfs5CoN7L7KpkfWNTWRI002xsfLri7MONC6gU4Ows
#nf+5uB4a1q5bI82/ylZ4Mp0dZpcZWguTp1HL4UzFQlM+YvwjsCC2eC55z7nP6Gzo
#LeZT3KU1zI8QpMnBO6v6Zy1Lo2MgG58BaHt+mt0X8nLqXaZa/lxMvLRgvL0QZbFB
#V5TjIx+ONwlhVpw8fe1Y33aEzUuiWfUQB8bhTRfxrjtlDsEqmF1gQOaFSRtxn7WD
#pbbp9jHDzSCLPWYZZA5q6g3Pw7TuBUWnjDZSkWWcGIOMUX4WpJmX378hcwhPL05V
#XxCYwJJy61elBfQX5Xw6VhE4GlYj8fnKPJmr9BijID2CDQt/z95EFBLoVb1z7kUQ
#DeSDly8mvORT3cAuEw/sIIsJNdKWKGaci7eU4XOjg6ntAc23lMSE9+4fWVBgH3JE
#fgL68LxXzPNhD694Nc4Wc7pjJiJMNn0gBsm28rpo8rbj9itEJaoUa1QNrSvQtVNe
#Bz0gMq/AysslqxRs+TrKPJXOCuBXehp2e1EW22gvHcEXEh3SjvH2vBKO6sFiVTxp
#aLnaTlytZtpkDy6rDlkxvQ793e4TNz+C9F9NeYtIDfV1RytQfaVyDdXR54JFD40Q
#KfWJYdeZihbem/w1R3Z1r22LaywqOA/JeM5tLRGYJUjmydwZYeUMVUsI5s51SGmy
#fNCBmDL34NOW0NnizNJnjVnd8OAa29GT0LvYItaILXX9AZ2K8bXTthlfHF0ajiLU
#RSMtizJrg8zKgi0VVoy5aaoE9totoMJ6+TnrNySITR8kTdkumYPDyGvhkXP1yTJ2
#gWkSMuz4reJi52rvSSEOyk+lFxHb8D5LWHMfnCKKaIUip8mH6sLvdDEZpJVbYwmY
#0Fm5Ytroo1hczle8LgQ0N+HcWDfCrbdraJc+697bNEQnK78uEQ+y7Qf+Dq6ZiWx7
#PMAz3ZrL3dg81fBWRxQm7rXgg12qDKnSWW7XAiMCSvzwug67Tfcsqb5GJ9QM0189
#vyIZXLuopWCc7CJVLZcV6XoqRtQYaQgJIqPHtZtdLy60C0QmCeXO6CODsaswlrHO
#GLIiQ9OQt4XzLh6UF0RJPrxuRPvEBTDIDtUg30tSCjn6EXodV8LGClEm5RmbFpXC
#wQsCQHXQUwEFhoO8fBhcMPSIEaAhL+tSLCpTT38O5JihW5G3f73VyWVlUePWFXlE
#gHWGc4xbadz9c7fa0KrlMk51M7KT0udMMdezhK11dXhHFAN5LyLWV48g0rdq5RKa
#JCqlo5ZvPrQxg3lM5YmRQbOiGXHmuxalVoRpbB3LNkHJuunEz+5ZjN2INYVJEnuo
#b9LCtzyiUFG48DuUQtZYoJi8NkC6oiZgMsH2yMSuYbCpQuTANgpfiRAH3oOmsk+b
#LNAMpsjbVUwK2SVeih68b4e3ES1LNVxpzWrKN3yl86b0n1TPmXgOC9Vvu6hR1nKa
#rs9O+1LvNzt1qXZhIGLfZ6FWYeKNSIzrJ1CUSl1Gpa6gUj5cYy1IOH3NyHS5crGG
#QI/Uq9eN9TdaZGuVY3Hj3D8kxyO5msfStxnF1Un0fyYYXqKx0/541W7uUISJZdUY
#qTgW3Jtvew0fG3lDPkCXuTuEIRRW+AxE8FuJTuUd737d3W7yI8UrwH199XdjY3sD
#b/iyeT7IvXvr1LQ58de5HdMQHYU+etJstU6DQcHTQoYGXjQ2kJYpxQTZ/giNP9je
#TOTVbLt4cKcE342CW/7xmKKhUbvtqZ8fbSE4PHTRJ4gN/9Zobew0G087f/vl9NEv
#TfhqPhAMSXYL3HYObLQeoMWDJ5vJg5/gvz+zsnRpFAuyNHLeS58q7/9f5H0TLAqw
#2mNAZiKcJHUtdUm6olb/ujcJq9BRa0UNyqugZ5J0ZFc3fEqqvjdP9fF+Fwh58+Xy
#S3JJ+lBaOUE7mtx12ocZu7qDdnNajxFwbpIv/S9Jv5Mm6Sm+xwY6bl9fdX+IdruH
#H+HPQ9B10VujnfSTpfOpiDY9QFV5jEZq/1EP4hjyTzo63nv75rj75uS3o4Pn+8fo
#qkQM9HHwSPKMdr188AQPM/JIt8MDf2KdozNlg2m3oaGXr37tpOgH3J2W6ak/0Nuj
#fLL4vC2GWTfrjEvpNq522/3ZIp8MpyAVyn5v0tj+m14Dp8XHcqO93aRJHzQwLq/y
#yZe4lCaa/P1qnPcTAI73wpM3i/FZVrwdHk7PQZEdvTNNlMkWrTV/J2JVlnpGVJRt
#VBLSJ6ZBedArAOcQUcC+Px8lW5Pk4qo1AYwVMl/XZOilHB3oJ1F2U0FyhtNi3NOh
#HkgglsEiVDo/GU8g68DRVbMOlwVXapk0HpkEAriVPFk2Yx5eEk+6EaPUdTyab8ij
#UG366sCKe4q3OdXHx49p7EYBPhq4owCQLVsvBgb0jgebjYVnXqvR18iPEURpoYwF
#t/Ps9AG4ezw29fqLz1t6aZyjw7wNDMII5qqqtxV8R3ndRaEk+J1cb+GrD1FyiwAl
#fsDgeCgvoxoaTaI65ggdow0cr90ixk6N6sFaB+2jCkc2RqY/MQ5jsFdSPYm+EEIj
#oKZzNurNSrw/lo+zhv5wGF0AEvS4n85JtpPGvz5ONpJ/fdxseuuFLrH1TNfCQvSX
#GwNYBVToUwYPIFUCMhUElBKUzgk9cqVLtvoZu/NivSjZNRcgq8LJe11PB/CnEaMC
#y4sk7kjKIWCIWoNZDUBdZDmuACkK604tmR+rcM2UYxePDxT6QnMVNlrGlTJ3nngx
#pTHAsqva+lg2Ph5vNH+OruMPnlTwon2DRTUDmkOAslXHSRv3J4SqTphcPXoQbbxO
#5lYIoKorb/ZRWMSMHku0WaZw1Q1fne8HwlWpPBQU7HxSKVp1IWKG7aqQJZxl0PmY
#uqBrNitDLSbJO90TsrzSvskTQE11rE1ZInRdQMpW6WLrSXpGniEJ2LF2282En95Q
#f7xqtmmHsxTdSbv3bx48WcJfP8noLv65FBZUEnf99Xgl38zwmjOPD4qaQmM89IOS
#6mDgzKZDR+QL9MJ5+NAmOvXXgqBVpgsac0bjiT88lqI8sx1swt6h3PDmhWkLOOUh
#/O+RNz19SgXFsQVfebD4Y8Odv7VPH30EefDoYxP1ctiWtnUaJjY/BqoqEeTpU4lJ
#oiNCCtitjXYS7GzxD5BIk9UDoqED+rqIVqCgMzQxGj+2S9CSZnLSyblVMUDVCyu2
#E2UR8hUI3W+jN5nqLPR3s9F7qxY78m7FD3jpkLTelh09mzVQ3TWn6toSp/pO50Pp
#OdK152HrvR6riqfhGbY4fVe+6fcasabWuRNYeZoc3ID3noljoVwiwXR0/KE3U1ME
#vVQoHgCAjOsR/7HIyMT6P/FfcXfXtmKjyAdX/E90BJzsKjBJe3t+B5CME95MEo/a
#MLxas0V5EYSE8btQTWYNJOKt6Jlg3Lsztq67hCFWwNhFq2ZLLF1dfJbGXvCRa6KD
#mrZAs9efy5SFlaGbFLjN9CsFneDh+7TwWGdv5pIZV5WsNGA36o3PBr2oSsB2uErP
#i+5wa29s1Rh/lL2c7DHw32B6VXK7DDdSVNxXU730uIV3zX208C4tWqNEYXQ4d/tq
#HEb9VQme1XAb6cgemm/xHCxlXMYfqVeaUhFj/DeOLJGh0dAleefwSR+vKP5xRfFC
#jC29xMFLb1sHGPPWdc56c9AZiWkE03Vk4E/zp8poEC7ANSIh1NSig+Cw5CPBMEBi
#1yFddVJW742lHlui4p4XiY6O4y7f7I6vuzbRpPWnPZNoFUZ9h9ZF0PE8ND5llofx
#qF+DdYesLN+12oFUewYsTuoNCm6xMCme8mkLxlsKWiO/oGzWBc2qYepSRtR6FkM0
#YIZa6wYnE+6TAdofmVa634IyX93vzukzbIlcwnzGM4MCWUG9GJqrCcbjFMXpZEVq
#jE4riFR/dm1nCep0zuGzd89hr/W9+LNoOhNd9vEGaO9eM0TfeMWVkXeBz+ldYFMS
#rYf2I0oSN/vtpJC4u+7aM2vhc0I3KqfzCwrn5cQK67IKxJX4L1mL3MoXjg3ym2z0
#AMmmrK2pTmhI4QRSr1fqqxOJer58M2kT1rYPMvi5+mdT6yviqW06ZlL5OoVOTtQZ
#N9dGVBm2JX7w8abR+dvyFDaiy8pDUqjFH8WtOifVDvPPPLwNFI63dYGjvFC7h6p4
#wcEHZcSURqgZJQYniJm7jCgyQLGmi8aj4pKyw0idUn65vPxiqqiE2RdaY5f3by4v
#2Vu2y2XwpF7A6RXgVQhwhOLDrNEE2KhvJuqlaXO/QV2QEEGP1u57VVcJ52rUwl6K
#Tvm16zScysDHnyagKla8zzi/nmX6gUakLU3wZdKglzQplXY6JgotXqe+f+NzmM1u
#rmkbE7OQl4wwnyprDZKgnUWefjFPpEzmFdyeBQ83KjZYNfyhvMBz2x/izat/OhsM
#g9MoPvEgwWsOE1LDDJNNCEYgtrzBztMozfZcwy4cZjGxK7yV2NpArapII3W4Agtt
#BraeeUEVjP6wENZHETaiVq3BAoEC6LnbVes30cMPqaoQ+XQJpFOLJ/GiaoHt9uaN
#DR6EkKIfGiaFD9dNXDiF9ue17wfdZ4uvCUpqNCZvCXBDtJiUF/lwHgxifIcQo4Al
#KgYU5SsmfNcsl5CLK3r19MUC2msFBLYbumay5JUwFIeoo5kOdBIZ9/QTsFGT85Dh
#DckZzaBETAXk09D1c7292v7By+fvD0+6L96/Otw/ODrG3RZVM/FG1WWttg4nab9m
#1/rnvrFT2O8SJnU2humuU17xa8jmy692mH02v/KzomfT357hlTH94Z6hcAnaE8Ym
#sFut7eOLHgyABKjSBNhj2JTqn3/t9fv4E7lcbWOZl37yNDlaTPBQmD5/1qQNHSq1
#Em7s0940CHMSkQhD8JwZt7nxkhdrdYP7pcx91G+ETxL5mFnsFSLjOEnrSrjTrngv
#ppHOMQSOe8np8f+HVzALfBzsSffx48f4X6v1L+Yn/tdUR+bl9TjSyryroqh0y4vM
#3vbbpQ+8i2TJ4ptlyV6oigmbX3BLOW7RTUsVvC5FO8+rt2hpzSaNjr7QnuL7Yokp
#oo8782kYXjGf0iFWi05ZvFzfyuO5H6k/8gwjrCa/jI/b8W8Hh4fpKZmiRAJGb0op
#2dGnw0tA37b63hnBigLqpErNphat+bT7S4WzcNqscRdWpn+HUFpepDFEAJWqrIj2
#2Un74wGd5AaF67XNikZiSzLnVWN01xZ3++iWny7Y9isppwacBSy+oRUb13Cq9fr4
#r8fby5XcDmAcj6fZ5DLVPI7/Mja/QSaXTL0kQ6gpE0wABc0fxjVYv/YYBD1NOuG9
#Jp/SzxrczhOMg2/rqVof9bcJw001dNrr/HM+MRYNnbafzfAMweKr3bpISJso8OTx
#gCeyoqQ9Hd/Vb2rJkeZeKxRWvJN29w/evXx1eJB6VkNz7EsRkuJH87UQJPV36cl2
#/52VXfNA066Nb0VHzItJ/h+QCOPOIYpXX+5mTeYv5Mgri7BvyAp8s2lkoNx61NQs
#dPNSafDBYMZuTphB1nUGjU/89Vf1h5JcDIx29hn2b0CxPUx/TWahtY5XNba8mocF
#KEOXue2xHUKbLN8xDwdbXB4Oz30T1wBd2nG3SOsR9+z/IWD+aD3+UZG57Rk3M9pZ
#BJz9lBeWq/Cue5OwKOfM22gDOdR7cJCD6bSVpQJWPJoqlRFxZfT+bkCRYF+cT2YL
#vN/JNsMelqseaWLSDFSj2aiHD337C6qa4G11iOhLYNlTHRBYw1y5ssrK8VoS2/CN
#0WcelKAERj7HuCCo68Pax0mxyyd8AIlnxoML6vfWIkyIZIu9UbKu/A6IZaTsyfOj
#Xw9OKC7qbvB8uyt2/Pb90d7BsSoXLgIk2LFJa5mNMDG+mqlk+vH7ly9ffVim/pPO
#vEUr/j0GirAQDsg9jb28qwUMjHi4p4OX26lvVUwUrv4L2nYVET3bZPPdBxRhSDI1
#akhLDNHMQEXOngcxSoiXRtUAxGnrvUhqTSUVvs+GlAis8olIZzGKXhEzfzr1UMRD
#k+K2WaCAxWgY95+OkSy0QWpSKkVBh7ZriEC3ob4h+8eezHG8pBMbgbgzQlmAIJ6B
#1QHtLeUqFWmVhBTxNUMBttw+zCf4Kkli7nfdvyFtK3IpxMzk5lJf7ELjs7lM5hg3
#eusrMlKrMLsLRnXmeCCsDkRqx8OG4IoFUq31Mv6mqrZS2vadw6FUX7pTyifPR/Mg
#TTeQKUpMkEtMZMZ3YMq/ffHvTpTC9+GrF+b7NMKIwRO7BDZ4SXnXe0oZ/1g9wAgV
#/y1dzbnBM7obphe+0Ihcl4iwlCbUkL9ni0hbSrLdg6f9m1soBgGlwmiy2Me6Q7Lr
#nkTecOV/lMDTL8P6WG4m8ReJQ+6r6uf6W87vycbaYhnoJ160WU2xtRQU8053O9lV
#fZa7C58DRoPK9e7+zYg+l8j8EaGLftcDdXnUI7Zp2anpKiFiJhNvcVnxr99IV7XC
#1dC2bd/N8jaC4R+xC2X0CBglllbvlqWxUY8mgqZUQRTCV8eY87Gs0EzupDxq2scK
#2hGlkh4OtcswZ4e91/sVHBGsFqldKivXPLuSGIFSxbnfeFZqy4ISI8lT7xXeyolY
#+QTZ7dvWpyrrtx0XAb4AcFTygmGsw1NrbkdW8UoVi/THA1JYkmemJSgJetrxXmp3
#fQiDz51oMHymxRGI45P9t+9PPHOjexKGooNWa/YOQhRrOTtDJr9/Y/q1XI/Rg5Fd
#xdCRO1i34bPZdZTJmFXGi8YqTTK7OuIiezKVe1KZuwv6cM8/va+bTEShbl7qZyas
#ZtQaZZNzUEZ+SZ40gS+iEh3/kCeztosKQ8ratXz9AfhE4SJx2zFzJWnLVzwCAKRh
#oBLnP94gAMY0P/f4ddEP9D6HcwLZXtfs3Te8N1n0VSARYDh1Xfrlm9fd/bcnr5+f
#7P3WBG10hKE+6U2xkETMEX4YUaoYjoszLBZCgAytR4o7Brp89FqBLFj0m03/0p0k
#bUcDO1UbITVk6O2s21658Y/owQ520Q/gehc1AcN6L6ZqIeya4dx6avktIhH0Syyg
#2rjtnzwcNEAFB6m3wSKeKnqrDlsSIPIAdtQ7+rE5dbCfEAVCM76LZiexqduiqtfB
#v+822TxUg0+Q6sbdJlVE+qwiZqAaBtpYjXGDXkTcNLFy24GsrI+Rg5UrCvRnDTuK
#m3TSUWbFZYaU8Z+vX+fMpmqI10KdN9DwBWNEmhoh6SU3oTTinrSjT3qbf9WqZWIT
#P6X16+c7a1nW1+SfuATaNm+xFkau/gRri3+YRufvMUqzGtXvu6jH99rJ390Me5j0
#Rni2dp0QwFJfaaGwlD0XzDqtfhPGu3rp3VIJZmq6B83N5TtNdSqUN6G9mWlNE2tN
#x7UOJj125Nxkf38Nc1p/qX/2NuArNfz9V8fPj193qxT9UEl2Xf0mptOaXSQp++0k
#mAvfbtd4mH3+77hrOzz4cIsB/TWbZIWau6PsM4hEHXiG3K6/+ZofMTh/w/FUToLB
#mBpfAGW6a7cDq7O3hARjr94gCZKNfQNGImqxfivsXustHmvwWL1p0F9avg0Xhtao
#VVz4/Gh9HqThQg6ErHneT0Z6IHtF/wL2GF9tdvp2LKaGdE0O+6qWnJ/sP0tGYW23
#nyFftPJLUq5wucBFxB2uaE8L6bE26/erT2P3lt5JM15xwQr/mWyPBv0g2sas3+2P
#6ZY6VH63h5KuDeX8nVy4NYzWPA9rfkMbbswBoHbeaBzXtMpyFrnDieFXH/5Zcxbu
#XBe43bmNPcv5fa+cTtKJ6yvZP+768a3mwt6HD6tmw6xL80EV9rgP0DrPJjZ3/+Dd
#rwdv6qMBcXh7deD2YtDknlMz8bujg3dHb4GNj7t7e8TK1Eh9SQU9VZcfsNnvpP5E
#HF/QSGGdR27hUCNIsdIY/u5dtVq1vuNCdBrrifBdvBG+q8qlTz5vr3L5BeQRatxL
#Eyc+2hZtOJ6N8bQQJhQe4dga+uz5gSLMabOTtqqOGHQRdL21acANpyFHAdv/6tjJ
#Fsahr/HE5Q/fU9G494p2Ka/vbCAJfA/Meln+T1d7w5gwkWNUKcUkrurg0hU93I9f
#Mbw9G4tLRn8UZgYt2F7RXsGMx78dvnoBsnglN643QYRzoEWjWT9lngVJGP7wXoNC
#3rU2tps7jc7ftk8fNR8s0d708cn9Gwt6+fHjT5XPJ/1TpzVR8rvNa35x7b/o7OZd
#uPscP/5t7VkOI/KN5/ntN3F1kzj7PC96vjNS2i2pqbrYznfuRD6ejTKdEfSi6sGu
#Fa9AoTUXtRDGCLHntKqYxlRHrtE/o4OLertnoZfvzJkXZ3d9hL6eav/nv5px9vjV
#/z5Y34iiOvjdtMd/ikUWr+7+dzTJ/vX53t7dbLJ09+uPZ5RVY9lumzE00tK9z6ZB
#tlHaPz/WfZf3VFyeXiyxiHJD1VluIQ68pKHEc3sQAbpDiJFYbP9AeNVhtLdXjRHl
#xTHaMwjdjQ64Gle3q3Pv0HJtTz98qOkqZVa0yC0s3GHU5H4FFepwMtl3wYrq3p5G
#+zWjsl89Jvs1mCh7693IU4eOzr09QlhxPcr8YOUsVTw8/P3182MtVX9IYki5Ig6v
#HxKOmS5h8cPcbyVp/ij4mlfVy+tJ3/jJWNMtLKq0xISWW+11OMpJ/JsYI+r1nb2j
#V+9OMM7In646qgI1nrDfreJM7X3M8g1rjvVEwt9QG/9hMVvkzfbscz4nZXI+BXX3
#MhsJCOrGefKWevyOFsl2+9XkEpb+wVt9Q/GXJHehf7XXRGu2gL1NPm35fk8ifyGy
#CJOfouthGDNHXdbGKMr6QmNDPZjMkWe53L/w6gJN+UQUeh2Gv8GAUeYho/NYP3e0
#tYXhyHeajZ32MwwE19yRoeDiT93ZUHBBUZWNV4s9PyqG64on7NzQtspR3s/uNR4z
#hUIqX2ewQ6l7xk+0K+gtaVx2exNzc7Thk5hnrktnHQBcZWIgI5HDIdIjzJoOK0dc
#9ouDCTpXzfJsovGndkEAXClieC6F6DATS88mGMhroEIkqqfQ7TUHPqF09OzkCwgJ
#disbv1rTSSPdek5RNrbQW9h78Z03XeGgwwGdKUCkNyf7r47SJrsVDqKQ+z9GYsfb
#cvYuMffNjTeZqSYtDX4/1o2aFPlWX0Ax83NFM8OEjgCaoaeuHM5YCDoO56VCF8d6
#a9jLRxhb3qN5LKg+soYuzptWsVkfr2hTBVHZushGM7+tiJRcG+w/Evt2myKLfett
#xTDbcsj+5nf1426uxUJ1hJ7SS16/3TfjQQn44IxwgNbvB5oslqMe95sOh/KULYLq
#YApTvMy7qknf09dAgk1bP7s1LE9Ax+2AvPsnqvs0H/2BFMJjjZl6qadNVpxNy3VY
#UJdcDzrCLaFWlN1SBT3RZZL7N0YT+/3g6PjV2zex2NgxZkQP+GkBq/29VUvGGgsN
#LMxsJtvWIm9tsmJ8+y8Uj3SvN0EvTWRt6CCrEuvck1jnREBHIWNiOpy72zcMIulr
#vId1ZgEXOTFCBK9r8mn0+UXG9UV8D0JH9mmlFRAOp9NP2QBfjMCYG9HuqPclgUf5
#4a5HLoe71Dgj3IuKK+vZZsAkm2wGbbplbzNYMjzFlYekA0XVDVm1o2/Wsg7xcbdd
#v39cq+Cy+unTg7cvf3iPKUjHB4+XSQfvnfRGxiB8mnS0ekP/uOQWPkn7w6+icBvA
#bj3fTEgRoCaOgSRIC0WtpAGqzATGG8UAninBZiRh+5WymeQTpXiNgOFayXsop4cF
#iPSPxURFIMWqxDVbJ6hbLeh56h+w8bOEQh8iCrT8P6NPhUo2VyoB80xu6DOJtspp
#KiAZqAAIwQzYs9+PCcIBDSQ+rDjIS/ppSrSgBrpV99Q+b6vMkCfmwKHYj2Q6NCX1
#aKhnGJJXQ+qFeoWAOLBUfUu3UsrRtRC0bnJAVnKUCVMTOpuSLbDhAqhNrQqAj1LE
#W8OdJLCNofdw8P0ISCBIVqWZX/TmQNfMwE7Ors3hDf6cTkbXKgvNkpYa+GFAYftQ
#z5HnLcbRvsoxkq3qsQKMUbwdcUoiYZmc58glxmDbm1y7AigzNHBE0BCllZwAVA3N
#AGdQf1DzbZzha6coN9B6SisvYjMrpnQMlJf2WR4ASayglDaqjryIH4gh391SuZfI
#L1wlc9yPCdCkMQyPpueq+VmRXebThQ42RI75tL1sDKaZ7ugUhplyCZpeMFSDF9gg
#6mOuISWdEkyshyYhgRKGsCZai3r2xs6WCT0OiyQ12lZjNO3DdLezxiy8ceVsqVso
#kr23h2+PsBXSXJ7Rp21GaTOoYCUN0KhAdi7m082EVCIN4QTrkghx3bVigjpLMhjf
#DC5Z37HLpdfn5IVlZoJF3KykE82UHhdIxGSK35Qw2np+gstTKcUacBLyN8GDH9fJ
#Re8y8yDhc6xTzS2XJF2UOkSVjhaTRH+PrpGZkRV7izIzrIaN0mULN8dVtwxbLUqa
#p0ok4K0xc5GeGjTaVMAsRoNak19wtXDriTkCAMWn28Wp0e3ipZkHqGkZhWxvlOPm
#kZbP50e//t6kGv8X0VCbvw==
-57
View File
@@ -1,57 +0,0 @@
macro_rules! options {
($($name:ident $sflag:literal $lflag:literal $hasarg:literal),* $(,)?) => {
enum Options {
$($name,)*
}
const SHORT_FLAGS: &[&str] = &[
$($sflag,)*
];
const LONG_FLAGS: &[&str] = &[
$($lflag,)*
];
const HAS_ARG: &[bool] = &[
$($hasarg,)*
];
};
}
options! {
Width "w" "width" true,
Height "h" "height" true,
}
#[derive(Default)]
pub struct Cli {
width: Option<usize>,
height: Option<usize>,
input_file: String,
}
impl Cli {
pub fn new() -> Self {
Cli::default()
}
pub fn run(&mut self) -> Option<()> {
let mut args = std::env::args();
/* Skip program name. */
args.next();
loop {
if let Some(arg) = args.next() {
if arg.starts_with("--") {
let flag = &arg[2..];
} else if arg.starts_with("-") {
let flag = &arg[1..2];
} else {
self.input_file = arg;
}
} else {
break;
}
}
Some(())
}
}
+72
View File
@@ -0,0 +1,72 @@
tree;
tree_prefix P;
ptype double;
<<header
/* User terminate codes. */
#define PARSER_TERMINATE_NUMBER_RANGE 1u
>>
<<
#include <charconv>
/* Store the numeric value of the matched text in *out. Returns false if the
* value cannot be represented. */
static bool parse_number(uint8_t const * match_text, size_t match_length, double * out)
{
const char * first = (const char *)match_text;
const char * last = first + match_length;
std::from_chars_result r = std::from_chars(first, last, *out);
return (r.ec == std::errc()) && (r.ptr == last);
}
>>
drop /\s+/;
token background;
token comma /,/;
token gt />/;
token height;
token lcurly /\{/;
token lt /</;
token options;
token rcurly /\}/;
token width;
token uint /\d+/ <<
if (!parse_number(match_text, match_length, &$$))
{
$terminate(PARSER_TERMINATE_NUMBER_RANGE);
}
>>
token number /-?\d+(\.\d+)?/ <<
if (!parse_number(match_text, match_length, &$$))
{
$terminate(PARSER_TERMINATE_NUMBER_RANGE);
}
>>
Start -> Options? SceneItems;
Background -> background Vector;
Height -> height uint;
Number -> uint;
Number -> number;
Options -> options lcurly OptionsItems rcurly;
OptionsItems -> ;
OptionsItems -> OptionsItem OptionsItems;
OptionsItem -> Background;
OptionsItem -> Height;
OptionsItem -> Width;
SceneItems -> ;
Vector -> lt Number comma Number comma Number gt;
Vector -> lt Number comma Number comma Number comma Number gt;
Width -> width uint;
+226
View File
@@ -0,0 +1,226 @@
#include <fart/Cli.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <stdint.h>
#define FLAGS \
FLAG(height, "h", true) \
FLAG(height_l, "height", true) \
FLAG(help, "help", false) \
FLAG(output, "o", true) \
FLAG(output_l, "output", true) \
FLAG(threads, "t", true) \
FLAG(threads_l, "threads", true) \
FLAG(version, "version", false) \
FLAG(width, "w", true) \
FLAG(width_l, "width", true) \
#define FLAG(name, flag, arg) flag_##name,
typedef enum
{
FLAGS
flag__count
} flag_id_t;
#undef FLAG
#define FLAG(name, flag, arg) static const char flag_##name##_str[] = flag;
FLAGS
#undef FLAG
typedef struct
{
const char * flag;
size_t flag_length;
bool arg;
} flag_def_t;
#define FLAG(name, flag, arg) { \
flag_##name##_str, \
sizeof(flag_##name##_str) - 1u, \
arg},
static const flag_def_t flag_defs[] = {
FLAGS
};
#undef FLAG
int Cli::run(int argc, char * argv[])
{
for (size_t i = 1; i < argc; i++)
{
size_t dashes = 0u;
if (strncmp(argv[i], "--", 2) == 0)
{
dashes = 2u;
}
else if (strncmp(argv[i], "-", 1) == 0)
{
dashes = 1u;
}
if (dashes > 0u)
{
const char * flag = &argv[i][dashes];
size_t flag_idx;
for (flag_idx = 0u; flag_idx < flag__count; flag_idx++)
{
size_t flag_length = flag_defs[flag_idx].flag_length;
size_t flag_dashes = flag_length == 1u ? 1u : 2u;
if (flag_dashes != dashes)
{
continue;
}
if (strncmp(flag, flag_defs[flag_idx].flag, flag_length) != 0)
{
continue;
}
if (dashes == 2 && flag[flag_length] != '\0' && flag[flag_length] != '=')
{
continue;
}
bool arg = flag_defs[flag_idx].arg;
const char * arg_value = nullptr;
if (arg)
{
if (flag[flag_length] == '\0')
{
if (i >= (argc - 1))
{
fprintf(stderr, "Error: option %s requires an argument\n", flag_defs[flag_idx].flag);
return 2;
}
i++;
arg_value = argv[i];
}
else if (dashes == 1u)
{
arg_value = &flag[flag_length];
}
else if (flag[flag_length + 1] != '\0')
{
arg_value = &flag[flag_length + 1];
}
else
{
fprintf(stderr, "Error: option %s requires an argument\n", flag_defs[flag_idx].flag);
return 2;
}
}
else if (dashes == 2u && flag[flag_length] == '=')
{
fprintf(stderr, "Error: option %s does not take an argument\n", flag_defs[flag_idx].flag);
return 2;
}
else
{
/* TODO: support combining short flags, e.g. -cofoo */
}
int r = handle_option(flag_idx, arg_value);
if (r != 0)
{
return r;
}
break;
}
if (flag_idx == flag__count)
{
fprintf(stderr, "Unknown flag: %s\n", flag);
return 2;
}
}
else
{
if (!this->input_file.empty())
{
fprintf(stderr, "Extraneous argument: %s\n", argv[i]);
return 2;
}
this->input_file = argv[i];
}
}
return 0;
}
static bool parse_size(const char * flag, const char * value, opt<size_t> * out)
{
bool valid = value[0] != '\0';
for (const char * c = value; *c != '\0'; c++)
{
if (*c < '0' || *c > '9')
{
valid = false;
break;
}
}
if (valid)
{
errno = 0;
unsigned long long v = strtoull(value, nullptr, 10);
if (errno == 0 && v <= SIZE_MAX)
{
*out = (size_t)v;
return true;
}
}
fprintf(stderr, "Error: invalid value for option %s: '%s'\n", flag, value);
return false;
}
int Cli::handle_option(size_t flag_idx, const char * arg_value)
{
switch (flag_idx)
{
case flag_height:
case flag_height_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.height))
{
return 2;
}
return 0;
case flag_help:
printf("Usage: fart [options] input-file\n");
printf("\n");
printf(" Options:\n");
printf("\n");
printf(" -h, --height H Set render height to H\n");
printf(" --help Show this help and exit\n");
printf(" -o, --output F Write rendered image to file F\n");
printf(" -t, --threads N Use N render threads (default: number of CPUs)\n");
printf(" -w, --width W Set render width to W\n");
printf(" --version Show fart version and exit\n");
return 1;
case flag_output:
case flag_output_l:
this->output_file = arg_value;
return 0;
case flag_threads:
case flag_threads_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.threads))
{
return 2;
}
if (*this->options.threads == 0u)
{
fprintf(stderr, "Error: option %s must be at least 1\n", flag_defs[flag_idx].flag);
return 2;
}
return 0;
case flag_width:
case flag_width_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.width))
{
return 2;
}
return 0;
case flag_version:
printf("fart, version 0.0.0.0.0.1\n");
return 1;
}
fprintf(stderr, "Internal error: unhandled flag_idx=%zu\n", flag_idx);
return 2;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <fart/Options.h>
class Cli
{
public:
Options options;
string_view input_file;
opt<string_view> output_file;
int run(int argc, char * argv[]);
private:
int handle_option(size_t option_idx, const char * arg_value);
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
struct Color
{
double r;
double g;
double b;
double a;
Color(double r = 0.0, double g = 0.0, double b = 0.0, double a = 1.0)
: r(r), g(g), b(b), a(a)
{
}
};
+84
View File
@@ -0,0 +1,84 @@
#include <fart/Engine.h>
#include <fart/Parser.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
Engine::Engine(const Options & options)
: options(options)
{
}
Engine::~Engine()
{
delete scene;
}
int Engine::run(string_view input_file, opt<string_view> output_file)
{
string output_path;
if (output_file)
{
output_path = *output_file;
}
else
{
string_view base = input_file;
if (base.ends_with(".fart"))
{
base.remove_suffix(5u);
}
output_path = string(base) + ".png";
}
image_writer.reset(ImageWriter::build(output_path));
if (!image_writer)
{
return 1;
}
/* Verify the output file can be written before rendering. */
FILE * fh = fopen(output_path.c_str(), "wb");
if (fh == nullptr)
{
fprintf(stderr, "Error opening output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
if (fclose(fh) != 0)
{
fprintf(stderr, "Error closing output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
if (remove(output_path.c_str()) != 0)
{
fprintf(stderr, "Error removing output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
delete scene;
scene = Parser::load_scene(input_file);
if (!scene)
{
return 1;
}
/* CLI options take precedence over scene options; defaults fill the rest. */
options.merge(scene->options);
options.apply_defaults();
Image image(*options.width, *options.height);
for (size_t y = 0u; y < *options.height; y++)
{
for (size_t x = 0u; x < *options.width; x++)
{
image[x, y] = *options.background;
}
}
if (!image_writer->write(image))
{
return 1;
}
return 0;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <fart/ImageWriter.h>
#include <fart/Options.h>
#include <fart/Scene.h>
#include <memory>
class Engine
{
public:
Engine(const Options & options);
~Engine();
int run(string_view input_file, opt<string_view> output_file);
protected:
Options options;
std::unique_ptr<ImageWriter> image_writer;
Scene * scene = nullptr;
};
+6
View File
@@ -0,0 +1,6 @@
#include <fart/Image.h>
Image::Image(size_t width, size_t height)
: width(width), height(height), pixels(width * height)
{
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <fart/Color.h>
#include <stddef.h>
struct Image
{
size_t width;
size_t height;
vector<Color> pixels;
Image(size_t width, size_t height);
Color & operator[](size_t x, size_t y)
{
return this->pixels[this->width * y + x];
}
};
+15
View File
@@ -0,0 +1,15 @@
#include <fart/ImageWriter.h>
#include <fart/PngImageWriter.h>
#include <stdio.h>
#include <filesystem>
ImageWriter * ImageWriter::build(const string & fname)
{
string ext = std::filesystem::path(fname).extension().string();
if (ext == ".png")
{
return new PngImageWriter(fname);
}
fprintf(stderr, "Unsupported output image format: '%s'\n", fname.c_str());
return nullptr;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <string>
#include <fart/Image.h>
class ImageWriter
{
public:
static ImageWriter * build(const string & fname);
virtual ~ImageWriter() = default;
virtual bool write(const Image & image) = 0;
protected:
ImageWriter(const string & fname) : fname(fname) {}
string fname;
};
+44
View File
@@ -0,0 +1,44 @@
#include <fart/Options.h>
#include <thread>
void Options::merge(const Options & other)
{
if (!width)
{
width = other.width;
}
if (!height)
{
height = other.height;
}
if (!threads)
{
threads = other.threads;
}
if (!background)
{
background = other.background;
}
}
void Options::apply_defaults()
{
if (!width)
{
width = 800u;
}
if (!height)
{
height = 600u;
}
if (!threads)
{
/* hardware_concurrency() may return 0 if it cannot be determined. */
size_t n = std::thread::hardware_concurrency();
threads = n > 0u ? n : 1u;
}
if (!background)
{
background = Color{0.0, 0.0, 0.0, 1.0};
}
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <fart/Color.h>
#include <stddef.h>
struct Options
{
opt<size_t> width;
opt<size_t> height;
opt<size_t> threads;
opt<Color> background;
/* Set each field that is not already set to the value from other. */
void merge(const Options & other);
/* Fill in any unset fields with default values. */
void apply_defaults();
};
+145
View File
@@ -0,0 +1,145 @@
#include <fart/Parser.h>
#include <fart/propane.h>
#include <fart/read_file.h>
#include <stdint.h>
#include <stdio.h>
Scene * Parser::load_scene(const string_view & input_file)
{
opt<vector<uint8_t>> input = read_file(input_file);
if (!input)
{
return nullptr;
}
p_context_t * context = p_context_new(input->data(), input->size());
size_t result = p_parse(context);
p_position_t pos = p_position(context);
int fname_len = (int)input_file.size();
const char * fname = input_file.data();
bool success = false;
switch (result)
{
case P_SUCCESS:
success = true;
break;
case P_DECODE_ERROR:
fprintf(stderr, "%.*s:%u:%u: error: invalid UTF-8 encoding\n",
fname_len, fname, pos.row, pos.col);
break;
case P_UNEXPECTED_INPUT:
fprintf(stderr, "%.*s:%u:%u: error: unexpected input\n",
fname_len, fname, pos.row, pos.col);
break;
case P_UNEXPECTED_TOKEN:
{
p_token_t token = p_token(context);
if (token == TOKEN___EOF)
{
fprintf(stderr, "%.*s:%u:%u: error: unexpected end of input\n",
fname_len, fname, pos.row, pos.col);
}
else
{
fprintf(stderr, "%.*s:%u:%u: error: unexpected token '%s'\n",
fname_len, fname, pos.row, pos.col, p_token_names[token]);
}
break;
}
case P_USER_TERMINATED:
if (p_user_terminate_code(context) == PARSER_TERMINATE_NUMBER_RANGE)
{
fprintf(stderr, "%.*s:%u:%u: error: number out of range\n",
fname_len, fname, pos.row, pos.col);
}
else
{
fprintf(stderr, "%.*s:%u:%u: internal error: unknown terminate code %zu\n",
fname_len, fname, pos.row, pos.col, p_user_terminate_code(context));
}
break;
default:
fprintf(stderr, "%.*s:%u:%u: internal error: unknown parser result %zu\n",
fname_len, fname, pos.row, pos.col, result);
break;
}
if (!success)
{
p_context_delete(context);
return nullptr;
}
Scene * scene = build_scene(input_file, p_result(context));
p_context_delete(context);
return scene;
}
/* Convert a uint token value to a size_t, reporting an error if it does not
* fit. */
static bool token_to_size(const string_view & input_file, PToken token, opt<size_t> * out)
{
/* SIZE_MAX + 1, computed so that it is exactly representable as a double. */
const double size_limit = (double)(SIZE_MAX / 2u + 1u) * 2.0;
double value = token.pvalue();
if (value >= size_limit)
{
p_position_t pos = token.position();
fprintf(stderr, "%.*s:%u:%u: error: value out of range\n",
(int)input_file.size(), input_file.data(), pos.row, pos.col);
return false;
}
*out = (size_t)value;
return true;
}
static Color vector_to_color(PVector vector)
{
Color color{
vector.pNumber2().pToken().pvalue(),
vector.pNumber4().pToken().pvalue(),
vector.pNumber6().pToken().pvalue(),
};
if (PNumber a = vector.pNumber8(); p_node_valid(a))
{
color.a = a.pToken().pvalue();
}
return color;
}
Scene * Parser::build_scene(const string_view & input_file, PStart start)
{
Scene * scene = new Scene();
/* If the options block is absent, pOptions() returns a null node and so
* does pOptionsItems() on it, so the loop body is skipped. */
for (POptionsItems items = start.pOptions().pOptionsItems(); p_node_valid(items); items = items.pOptionsItems())
{
POptionsItem item = items.pOptionsItem();
bool ok = true;
if (PBackground background = item.pBackground(); p_node_valid(background))
{
scene->options.background = vector_to_color(background.pVector());
}
else if (PHeight height = item.pHeight(); p_node_valid(height))
{
ok = token_to_size(input_file, height.pToken2(), &scene->options.height);
}
else if (PWidth width = item.pWidth(); p_node_valid(width))
{
ok = token_to_size(input_file, width.pToken2(), &scene->options.width);
}
if (!ok)
{
delete scene;
return nullptr;
}
}
return scene;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <fart/Scene.h>
struct PStart;
class Parser
{
public:
static Scene * load_scene(const string_view & input_file);
private:
static Scene * build_scene(const string_view & input_file, PStart start);
};
+38
View File
@@ -0,0 +1,38 @@
#include <fart/PngImageWriter.h>
#include <png.h>
#include <stdint.h>
#include <stdio.h>
#include <algorithm>
#include <cmath>
static uint8_t to_byte(double v)
{
return (uint8_t)std::lround(std::clamp(v, 0.0, 1.0) * 255.0);
}
bool PngImageWriter::write(const Image & image)
{
vector<uint8_t> buffer(image.width * image.height * 4u);
for (size_t i = 0u; i < image.pixels.size(); i++)
{
const Color & c = image.pixels[i];
buffer[i * 4u + 0u] = to_byte(c.r);
buffer[i * 4u + 1u] = to_byte(c.g);
buffer[i * 4u + 2u] = to_byte(c.b);
buffer[i * 4u + 3u] = to_byte(c.a);
}
png_image png = {};
png.version = PNG_IMAGE_VERSION;
png.width = image.width;
png.height = image.height;
png.format = PNG_FORMAT_RGBA;
if (!png_image_write_to_file(&png, fname.c_str(), 0, buffer.data(), 0, nullptr))
{
fprintf(stderr, "Error writing PNG file '%s': %s\n", fname.c_str(), png.message);
png_image_free(&png);
return false;
}
return true;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <fart/ImageWriter.h>
class PngImageWriter : public ImageWriter
{
public:
PngImageWriter(const string & fname) : ImageWriter(fname) {}
bool write(const Image & image) override;
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <fart/Options.h>
class Scene
{
public:
Options options;
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using string = std::string;
using string_view = std::string_view;
template <typename T> using opt = std::optional<T>;
template <typename T> using vector = std::vector<T>;
+20
View File
@@ -0,0 +1,20 @@
#include <fart/Cli.h>
#include <fart/Engine.h>
#include <stdio.h>
int main(int argc, char * argv[])
{
Cli cli;
int rc = cli.run(argc, argv);
if (rc > 0)
{
return rc - 1;
}
if (cli.input_file.empty())
{
fprintf(stderr, "Error: no input file specified\n");
return 1;
}
Engine engine(cli.options);
return engine.run(cli.input_file, cli.output_file);
}
+32
View File
@@ -0,0 +1,32 @@
#include <fart/read_file.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
opt<vector<uint8_t>> read_file(string_view fname)
{
string path(fname);
FILE * fh = fopen(path.c_str(), "rb");
if (fh == nullptr)
{
fprintf(stderr, "Error opening input file '%s': %s\n", path.c_str(), strerror(errno));
return std::nullopt;
}
vector<uint8_t> buffer;
uint8_t chunk[4096];
size_t n;
while ((n = fread(chunk, 1u, sizeof(chunk), fh)) > 0u)
{
buffer.insert(buffer.end(), chunk, chunk + n);
}
if (ferror(fh))
{
fprintf(stderr, "Error reading input file '%s': %s\n", path.c_str(), strerror(errno));
fclose(fh);
return std::nullopt;
}
fclose(fh);
return buffer;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
opt<vector<uint8_t>> read_file(string_view fname);
-16
View File
@@ -1,16 +0,0 @@
mod cli;
use crate::cli::Cli;
use std::process::ExitCode;
fn main() -> ExitCode {
match run() {
Some(_) => ExitCode::SUCCESS,
None => ExitCode::FAILURE,
}
}
fn run() -> Option<()> {
let mut cli = Cli::new();
cli.run()
}