Edited at
#学生LTDay 19

コンパイルできるアスキーアートを作る

More than 1 year has passed since last update.

こんにちは。こーでぃです。

IOCCCというコンテストを知っていますか?

日本語にすると"国際読みにくいCコードコンテスト"です。

その名の通りC言語コードの読みにくさを競う大会なのですが、入賞作品の中に、コード自体がアスキーアートになっているものがあります()。

このような、コードがアスキーアートになったもの、すなわちコンパイルできるアスキーアートを自動で作ろう、というのがこの記事の趣旨です。

(コードが控えめに言ってカオスなので記事にはかなり略したものを載せています。)

(仕組みとかどうでもいいよ!って方は完成形をご覧ください)


流れ

大きく分けて以下の3工程で作っていきます。

1. トークン分割

2. ドット絵化

3. 置換

例えばこのコード


twist.cpp


/**
* ひねるあれ
* ex) $ ./a.out Hello
* Hello
* elloH
* lloHe
* loHel
* oHell
*/

#include <iostream>
#include <string>

int main(int argc, char const *argv[]) {
std::string str = argc != 2 ? "Hello, World!" : argv[1];
for(auto it = str.cbegin(); it != str.cend(); it++){
int i = 0;
for(int j = 0; j < str.length(); j++, i++){
if(it + i != str.cend()){
std::cout << *(it + i);
}else{
i = std::distance(it, str.cbegin());
std::cout << *(it + i);
}
}
std::cout << std::endl;
}
}


https://wandbox.org/permlink/QJDXXM2EtejTfXZN

このコードでOctocatの形のAAを作りたいとします。

まずは、間にいくつスペース/改行を挟んでも意味が変わらないようなトークン列にソースコードを分割します。

[" ", "", " ", "", "#include<iostream>", " ", "", " ", "", "#include<string>", "int ", "main ", "(", "int ", "argc ", ",", "char ", "const ", "*", "argv ", "[", "]", ")", "{", "std ", "::", "string ", "str ", "=", "argc ", "!=", "2 ", "?", ""H"", ""e"", ""l"", ""l"", ""o"", "","", "" "", ""W"", ""o"", ""r"", ""l"", ""d"", ""!"", ":", "argv ", "[", "1 ", "]", ";", "for ", "(", "auto ", "it ", "=", "str ", ".", "cbegin ", "(", ")", ";", "it ", "!=", "str ", ".", "cend ", "(", ")", ";", "it ", "++", ")", "{", "int ", "i ", "=", "0 ", ";", "for ", "(", "int ", "j ", "=", "0 ", ";", "j ", "<", "str ", ".", "length ", "(", ")", ";", "j ", "++", ",", "i ", "++", ")", "{", "if ", "(", "it ", "+", "i ", "!=", "str ", ".", "cend ", "(", ")", ")", "{", "std ", "::", "cout ", "<<", "*", "(", "it ", "+", "i ", ")", ";", "}", "else ", "{", "i ", "=", "std ", "::", "distance ", "(", "it ", ",", "str ", ".", "cbegin ", "(", ")", ")", ";", "std ", "::", "cout ", "<<", "*", "(", "it ", "+", "i ", ")", ";", "}", "}", "std ", "::", "cout ", "<<", "std ", "::", "endl ", ";", "}", "}", ]

そしてOctocatくんの画像を適当な大きさのブロックに分割し、それぞれについて黒が多かったら先ほどのトークンを置き、白が多かったらスペースを置き...としていきます


(Octocatに見えない)

                  #####                   ####                  

#######
##########
##########
##########
############
#############
### ####
## ##
## ###
# ##
### ##
## ##
## ##
## ##
## #######
# ######
## #####
######
## #### #
# ####
#######
####
#####
## # ####
# ##

#(=黒)の部分に先程のトークンを乗せていくと、下のようになります。


octocat.cpp


#include<iostream>
#include<string>

int
main (int argc ,char const
*argv []){std ::string str
=argc !=2 ?"H""e""l""l""o"
","" ""W""o""r""l""d""!":argv
[1 ];for (auto it =str .cbegin
();it !=str .cend ();it ++
){int i =0 ;
for ( int j
=0 ;j <
str .length
(); j ++
,i ++)
{if (it
+i !=str
.cend ()){std ::cout
<< *(it +i )
;} else {i =std
::distance (it ,str
.cbegin ()); std
:: cout <<*(
it +i );}}
std ::cout <<
std ::endl ;}
}// // ////////
// ////


これで完成です。

もちろんこのコードはもちろんコンパイルでき、それは変換前と同じ挙動を示します。

https://wandbox.org/permlink/SmyFAN87CF6CcD1O

さて、各工程を詳しく解説していきます


1.トークン分割

まずはアスキーアートにしたいC/C++のソースコードを、トークンに分割します。

C/C++のソースを扱えるライブラリにはlibToolingというC++向けのライブラリとlibclangというC向けのライブラリがあるのですが、libToolingを使うほど大規模でもないので、libclangで書きます。

下のようなコードになります(参考)

std::vector<std::string> tokenize(const std::string& filepath) {

std::vector<std::string> dest;

auto const index = clang_createIndex(1, 0); // exclude_decls_from_pch:1, display_diagnostics:0
clang_CXIndex_setGlobalOptions(index, CXGlobalOpt_None);
const char* args[] = { "-Xclang", "-cc1" };
auto unit = clang_createTranslationUnitFromSourceFile(index, filepath.c_str(),
sizeof(args) / sizeof(char*), args,
0, nullptr);
if (unit != NULL) {
auto const cursor = clang_getTranslationUnitCursor(unit);
auto const range = clang_getCursorExtent(cursor);
CXToken* tokens = nullptr;
unsigned int num_tokens = 0;
clang_tokenize(unit, range, &tokens, &num_tokens);

std::pair<std::string, int> macro_holder = {"", -1};
for (int i = 0; i < num_tokens; ++i) {
auto const& token = tokens[i];
auto const spelling = clang_getTokenSpelling(unit, token);
CXFile file;
unsigned line, column, offset;
clang_getSpellingLocation(clang_getTokenLocation(unit, token), &file, &line, &column, &offset);
std::string thes = clang_getCString(spelling);
if(*std::cbegin(thes) == '"' && *(std::cend(thes) - 1) == '"' && thes.length() != 2 && macro_holder.second == -1){
auto it = std::cbegin(thes) + 1;
auto end = std::cend(thes) - 1;
for(; it != end; it++){
if(*it == '\\'){
it++;
dest.push_back(std::string("\"\\") + *it + "\"");
}else
dest.push_back(std::string("\"") + *it + "\"");
}
}else if (*std::cbegin(thes) == '#' && macro_holder.second == -1) { //start of macros
macro_holder.first = "#";
macro_holder.second = line;
}else if (*std::cbegin(thes) == '#' && macro_holder.second != -1 && macro_holder.second != line) { //end & start of macros
dest.push_back(macro_holder.first+'\n');
macro_holder.first = "#";
macro_holder.second = line;
}else{
if(macro_holder.second != -1){
if(macro_holder.second == line){ // in macros
macro_holder.first += thes;
thes = "";
}else{ // end of macros
dest.push_back(macro_holder.first);
macro_holder.first = "";
macro_holder.second = -1;
}
}
auto const kind = clang_getTokenKind(token);
if(kind != CXToken_Comment){
if(kind != CXToken_Punctuation)
dest.push_back(thes+" ");
else
dest.push_back(thes);
}
}
clang_disposeString(spelling);
}
clang_disposeTokens(unit, tokens, num_tokens);
clang_disposeTranslationUnit(unit);
} else {
std::cerr << "Failed to tokenize: \"" << filepath << "\"" << std::endl;
}
clang_disposeIndex(index);
return dest;
}

clang_getTokenKind(token)で得られたCXTokenKindから、トークン後にスペースを挟むべきか否かを判断しています。

ここでコメント(CXToken_Comment)は邪魔なので消しています。

また各トークンをできるだけ短くしたいので、文字列は文字ごとに分割しています。("ABCD" -> "A" "B" "C" "D")

さらにプリプロセッサ/マクロは分割されると困るので、macro_holder辺りで1行に固めて返すようにしています。(#includeなど)

さて、これでそれぞれの間にいくつスペース/改行を挟んでもコンパイルできるトークン列ができました。

[" ", "", " ", "", "#include<iostream>", " ", "", " ", "", "#include<string>", "int ", "main ", "(", "int ", "argc ", ",", "char ", "const ", "*", "argv ", "[", "]", ")", "{", "std ", "::", "string ", "str ", "=", "argc ", "!=", "2 ", "?", ""H"", ""e"", ""l"", ""l"", ""o"", "","", "" "", ""W"", ""o"", ""r"", ""l"", ""d"", ""!"", ":", "argv ", "[", "1 ", "]", ";", "for ", "(", "auto ", "it ", "=", "str ", ".", "cbegin ", "(", ")", ";", "it ", "!=", "str ", ".", "cend ", "(", ")", ";", "it ", "++", ")", "{", "int ", "i ", "=", "0 ", ";", "for ", "(", "int ", "j ", "=", "0 ", ";", "j ", "<", "str ", ".", "length ", "(", ")", ";", "j ", "++", ",", "i ", "++", ")", "{", "if ", "(", "it ", "+", "i ", "!=", "str ", ".", "cend ", "(", ")", ")", "{", "std ", "::", "cout ", "<<", "*", "(", "it ", "+", "i ", ")", ";", "}", "else ", "{", "i ", "=", "std ", "::", "distance ", "(", "it ", ",", "str ", ".", "cbegin ", "(", ")", ")", ";", "std ", "::", "cout ", "<<", "*", "(", "it ", "+", "i ", ")", ";", "}", "}", "std ", "::", "cout ", "<<", "std ", "::", "endl ", ";", "}", "}", ]

これをうまく並べて絵にします


2,3.ドット絵化、置換

OpenCVを使います。

元画像を二値化したものを適当なサイズのブロックに分けてそれぞれ処理していきます。

// src_image: cv::thresholdで2値化済み

// assert(!(src_image.rows % rows || src_image.cols % cols));
for(int y = 0; y < src_image.rows; y+=rows){ // 縦に...
for(int x = 0; x < src_image.cols; x+=cols){ // 横に...
auto part_image = src_image(cv::Rect(x, y, cols, rows)); // 部分画像

int b = std::count_if(part_image.begin<unsigned char>(), part_image.end<unsigned char>(), [](auto x) -> bool { return x; });
if(b > part_image.total() / 2){
/* 略: 1で作ったtokenを置く、置いた文字数だけxを進める */
}else{
/* スペースを置く */
}
}
}

std::count_ifcv::Matのiteratorをぶん投げることによってコードが簡潔になっています。

bに黒ピクセルの数が入るのでtotal() / 2より大きかったら(=過半数が黒だったら)そのブロックは黒と判断します。

そうして黒と判断されたブロックに先程のトークンを雑に置いてAAの出来上がりです。


octocat.cpp


#include<iostream>
#include<string>

int
main (int argc ,char const
*argv []){std ::string str
=argc !=2 ?"H""e""l""l""o"
","" ""W""o""r""l""d""!":argv
[1 ];for (auto it =str .cbegin
();it !=str .cend ();it ++
){int i =0 ;
for ( int j
=0 ;j <
str .length
(); j ++
,i ++)
{if (it
+i !=str
.cend ()){std ::cout
<< *(it +i )
;} else {i =std
::distance (it ,str
.cbegin ()); std
:: cout <<*(
it +i );}}
std ::cout <<
std ::endl ;}
}// // ////////
// ////



#defineでさらにきれいに

変換したいコード中にclang_disposeTranslationUnitみたいな長いシンボルがあると、どうしてもそれが形を崩してしまいます。

そのためそういった長いものをabみたいな短い名前に#defineしてやることで、さらにきれいなAAができるようになります。

#include<iostream>

#include<string>
#define c cbegin
#define a const
#define e distance
#define d length
#define b string

int
main (int argc ,char a *argv
[]){std ::b str =argc !=2
?"H""e""l""l""o"","" ""W"
"o""r""l""d""!":argv [1 ];
for (auto it =str .c ();it !=
str .cend ();it ++){int i =
0 ;for (int j
=0 ; j <str
.d ( );j
++, i ++
){if (it
+i !=str
.cend ()
){ std
:: cout <<*(it +i
); }else {i =
std ::e (it ,str
.c ());std ::cout
<<*(it +i );}}
std ::cout <<
std ::endl ;
}}////////
////////////
////// ////////
// ////


変換プログラムの完成形

coord-e/cart

(C言語とartをひそかに掛けてcart)

インストールしたら

cart source.cpp image.png -r 2 -c 1 > out.cpp

のように手軽に変換できます。

-r-cの値を色々変えると大きさや解像度(?)が変わります。


Showcase

先程のOctocatくんの例だとコード量が少なかったので潰れた感じになってしまいましたが、たくさんコードを投げてやって1ブロックを小さくしてやるとそれっぽいものができます。

下の画像はcartのソースコードをcart自身に変換させたものです。

Screenshot from 2017-12-19 21-13-37.png

Octocat

Screenshot from 2017-12-19 19-09-46.png

ピカチュー

Screenshot from 2017-12-19 21-09-08.png

D言語くん

※画像にあるのが全コードではありません、AAの前にたくさん#defineが並んでいます


雑感

用途が見当たらないものを作ってしまった。

もう少し頭を使って配置するよう改良していきたい。