mirror of
https://github.com/cookiengineer/audacity
synced 2025-10-20 09:31:15 +02:00
Crashreporting
This commit is contained in:
41
crashreports/crashreporter/CMakeLists.txt
Normal file
41
crashreports/crashreporter/CMakeLists.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
#Adds a Crash Reporting dialog which may be invoked by a crashing program
|
||||
|
||||
set(TARGET crashreporter)
|
||||
set(TARGET_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
message( STATUS "========== Configuring ${TARGET} ==========" )
|
||||
|
||||
set(SOURCES
|
||||
PRIVATE
|
||||
warning.xpm
|
||||
CrashReportApp.h
|
||||
CrashReportApp.cpp
|
||||
)
|
||||
|
||||
add_executable(${TARGET})
|
||||
target_sources(${TARGET} ${SOURCES})
|
||||
target_link_libraries(${TARGET} breakpad::processor breakpad::sender wxwidgets::wxwidgets)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(${TARGET} PROPERTIES WIN32_EXECUTABLE ON)
|
||||
endif()
|
||||
|
||||
if( CMAKE_SYSTEM_NAME MATCHES "Darwin" )
|
||||
add_custom_command(
|
||||
TARGET
|
||||
${TARGET}
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -D SRC="${_EXEDIR}/crashreporter"
|
||||
-D DST="${_PKGLIB}"
|
||||
-D WXWIN="${_SHARED_PROXY_BASE_PATH}/$<CONFIG>"
|
||||
-P ${AUDACITY_MODULE_PATH}/CopyLibs.cmake
|
||||
POST_BUILD
|
||||
)
|
||||
elseif(UNIX)
|
||||
target_compile_definitions(${TARGET} PRIVATE -DINSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}")
|
||||
install(TARGETS ${TARGET} RUNTIME)
|
||||
endif()
|
||||
|
||||
set_target_property_all( ${TARGET} RUNTIME_OUTPUT_DIRECTORY "${_EXEDIR}" )
|
||||
|
||||
organize_source( "${TARGET_ROOT}" "" "${SOURCES}" )
|
464
crashreports/crashreporter/CrashReportApp.cpp
Normal file
464
crashreports/crashreporter/CrashReportApp.cpp
Normal file
@@ -0,0 +1,464 @@
|
||||
#include "CrashReportApp.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <memory>
|
||||
|
||||
#include <wx/cmdline.h>
|
||||
#include <wx/chartype.h>
|
||||
#include <wx/artprov.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/stdpaths.h>
|
||||
|
||||
#include "google_breakpad/processor/basic_source_line_resolver.h"
|
||||
#include "google_breakpad/processor/minidump_processor.h"
|
||||
#include "google_breakpad/processor/process_state.h"
|
||||
#include "google_breakpad/processor/minidump.h"
|
||||
#include "processor/stackwalk_common.h"
|
||||
|
||||
#include "warning.xpm"
|
||||
|
||||
//Temporary solution until lib-strings is added
|
||||
#define XC(msg, ctx) (wxGetTranslation(msg, wxEmptyString, ctx))
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
#include "client/windows/sender/crash_report_sender.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring ToPlatformString(const std::string& utf8)
|
||||
{
|
||||
return std::wstring_convert<std::codecvt_utf8<std::wstring::traits_type::char_type>, std::wstring::traits_type::char_type>().from_bytes(utf8);
|
||||
}
|
||||
|
||||
bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map<std::string, std::string>& arguments, const wxString& commentsFilePath)
|
||||
{
|
||||
std::map<std::wstring, std::wstring> files;
|
||||
files[L"upload_file_minidump"] = minidumpPath.wc_str();
|
||||
if (!commentsFilePath.empty())
|
||||
{
|
||||
files[wxFileName(commentsFilePath).GetFullName().wc_str()] = commentsFilePath.wc_str();
|
||||
}
|
||||
|
||||
std::map<std::wstring, std::wstring> parameters;
|
||||
for (auto& p : arguments)
|
||||
{
|
||||
parameters[ToPlatformString(p.first)] = ToPlatformString(p.second);
|
||||
}
|
||||
|
||||
google_breakpad::CrashReportSender sender(L"");
|
||||
|
||||
auto result = sender.SendCrashReport(
|
||||
ToPlatformString(url),
|
||||
parameters,
|
||||
files,
|
||||
nullptr
|
||||
);
|
||||
return result == google_breakpad::RESULT_SUCCEEDED;
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
||||
#include "common/linux/http_upload.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map<std::string, std::string>& arguments, const wxString& commentsFilePath)
|
||||
{
|
||||
std::map<std::string, std::string> files;
|
||||
files["upload_file_minidump"] = minidumpPath.ToStdString();
|
||||
if (!commentsFilePath.empty())
|
||||
{
|
||||
files["comments.txt"] = commentsFilePath.ToStdString();
|
||||
}
|
||||
|
||||
std::string response, error;
|
||||
bool success = google_breakpad::HTTPUpload::SendRequest(
|
||||
url,
|
||||
arguments,
|
||||
files,
|
||||
std::string(),
|
||||
std::string(),
|
||||
std::string(),
|
||||
&response,
|
||||
NULL,
|
||||
&error);
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
IMPLEMENT_APP(CrashReportApp);
|
||||
namespace
|
||||
{
|
||||
std::map<std::string, std::string> parseArguments(const std::string& str)
|
||||
{
|
||||
int TOKEN_IDENTIFIER{ 0 };
|
||||
constexpr int TOKEN_EQ{ 1 };
|
||||
constexpr int TOKEN_COMMA{ 2 };
|
||||
constexpr int TOKEN_VALUE{ 3 };
|
||||
|
||||
int i = 0;
|
||||
|
||||
std::string key;
|
||||
int state = TOKEN_COMMA;
|
||||
std::map<std::string, std::string> result;
|
||||
while (true)
|
||||
{
|
||||
if (str[i] == 0)
|
||||
break;
|
||||
else if (isspace(str[i]))
|
||||
++i;
|
||||
else if (isalpha(str[i]))
|
||||
{
|
||||
if (state != TOKEN_COMMA)
|
||||
throw std::logic_error("malformed parameters string: unexpected identifier");
|
||||
|
||||
int begin = i;
|
||||
while (isalnum(str[i]))
|
||||
++i;
|
||||
|
||||
key = str.substr(begin, i - begin);
|
||||
state = TOKEN_IDENTIFIER;
|
||||
}
|
||||
else if (str[i] == '=')
|
||||
{
|
||||
if (state != TOKEN_IDENTIFIER)
|
||||
throw std::logic_error("malformed parameters string: unexpected '=' symbol");
|
||||
++i;
|
||||
state = TOKEN_EQ;
|
||||
}
|
||||
else if (str[i] == '\"')
|
||||
{
|
||||
if (state != TOKEN_EQ)
|
||||
throw std::logic_error("malformed parameters string: unexpected '\"' symbol");
|
||||
|
||||
int begin = ++i;
|
||||
while (true)
|
||||
{
|
||||
if (str[i] == 0)
|
||||
throw std::logic_error("unterminated string literal");
|
||||
else if (str[i] == '\"')
|
||||
{
|
||||
if (i > begin)
|
||||
result[key] = str.substr(begin, i - begin);
|
||||
else
|
||||
result[key] = std::string();
|
||||
++i;
|
||||
state = TOKEN_VALUE;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else if (str[i] == ',')
|
||||
{
|
||||
if (state != TOKEN_VALUE)
|
||||
throw std::logic_error("malformed parameters string: unexpected ',' symbol");
|
||||
state = TOKEN_COMMA;
|
||||
++i;
|
||||
}
|
||||
else
|
||||
throw std::logic_error("malformed parameters string");
|
||||
}
|
||||
if (state != TOKEN_VALUE)
|
||||
throw std::logic_error("malformed parameters string");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PrintMinidump(google_breakpad::Minidump& minidump)
|
||||
{
|
||||
google_breakpad::BasicSourceLineResolver resolver;
|
||||
google_breakpad::MinidumpProcessor minidumpProcessor(nullptr, &resolver);
|
||||
google_breakpad::MinidumpThreadList::set_max_threads(std::numeric_limits<uint32_t>::max());
|
||||
google_breakpad::MinidumpMemoryList::set_max_regions(std::numeric_limits<uint32_t>::max());
|
||||
|
||||
google_breakpad::ProcessState processState;
|
||||
|
||||
if (minidumpProcessor.Process(&minidump, &processState) != google_breakpad::PROCESS_OK)
|
||||
{
|
||||
printf("Failed to process minidump");
|
||||
}
|
||||
else
|
||||
{
|
||||
google_breakpad::PrintProcessState(processState, true, &resolver);
|
||||
}
|
||||
}
|
||||
|
||||
wxString MakeDumpString(google_breakpad::Minidump& minidump, const wxString& temp)
|
||||
{
|
||||
#if _WIN32
|
||||
auto stream = _wfreopen(temp.wc_str(), L"w+", stdout);
|
||||
#else
|
||||
auto stream = freopen(temp.utf8_str().data(), "w+", stdout);
|
||||
#endif
|
||||
if (stream == NULL)
|
||||
throw std::runtime_error("Failed to print minidump: cannot open temp file");
|
||||
PrintMinidump(minidump);
|
||||
fflush(stdout);
|
||||
|
||||
auto length = ftell(stream);
|
||||
std::vector<char> bytes(length);
|
||||
fseek(stream, 0, SEEK_SET);
|
||||
fread(&bytes[0], 1, length, stream);
|
||||
fclose(stream);
|
||||
|
||||
#if _WIN32
|
||||
_wremove(temp.wc_str());
|
||||
#else
|
||||
remove(temp.utf8_str().data());
|
||||
#endif
|
||||
|
||||
return wxString::From8BitData(&bytes[0], bytes.size());
|
||||
}
|
||||
|
||||
wxString MakeHeaderString(google_breakpad::Minidump& minidump)
|
||||
{
|
||||
if (auto exception = minidump.GetException())
|
||||
{
|
||||
if (auto rawException = exception->exception())
|
||||
{
|
||||
// i18n-hint C++ programming assertion
|
||||
return wxString::Format(_("Exception code 0x%x"), rawException->exception_record.exception_code);
|
||||
}
|
||||
else
|
||||
{
|
||||
// i18n-hint C++ programming assertion
|
||||
return _("Unknown exception");
|
||||
}
|
||||
}
|
||||
else if (auto assertion = minidump.GetAssertion())
|
||||
{
|
||||
auto expression = assertion->expression();
|
||||
if (!expression.empty())
|
||||
{
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
return _("Unknown error");
|
||||
}
|
||||
|
||||
void DoShowCrashReportFrame(const wxString& header, const wxString& dump, const std::function<bool(const wxString& comment)>& onSend)
|
||||
{
|
||||
static constexpr int MaxUserCommentLength = 2000;
|
||||
|
||||
auto frame = new wxFrame(
|
||||
nullptr,
|
||||
wxID_ANY,
|
||||
_("Problem Report for Audacity"),
|
||||
wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)//disable frame resize
|
||||
);
|
||||
frame->SetOwnBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK));
|
||||
|
||||
auto mainLayout = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto headerText = new wxStaticText(frame, wxID_ANY, header);
|
||||
headerText->SetFont(wxFont(wxFontInfo().Bold()));
|
||||
|
||||
auto headerLayout = new wxBoxSizer(wxHORIZONTAL);
|
||||
headerLayout->Add(new wxStaticBitmap(frame, wxID_ANY, wxIcon(warning)));
|
||||
headerLayout->AddSpacer(5);
|
||||
headerLayout->Add(headerText, wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
|
||||
|
||||
auto buttonsLayout = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
wxTextCtrl* commentCtrl = nullptr;
|
||||
if (onSend != nullptr)
|
||||
{
|
||||
commentCtrl = new wxTextCtrl(frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(500, 100), wxTE_MULTILINE);
|
||||
commentCtrl->SetMaxLength(MaxUserCommentLength);
|
||||
}
|
||||
|
||||
if (onSend != nullptr)
|
||||
{
|
||||
auto okButton = new wxButton(frame, wxID_ANY, XC("&Don't send", "crash reporter button"));
|
||||
auto sendButton = new wxButton(frame, wxID_ANY, XC("&Send", "crash reporter button"));
|
||||
|
||||
okButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&)
|
||||
{
|
||||
frame->Close(true);
|
||||
});
|
||||
sendButton->Bind(wxEVT_BUTTON, [frame, commentCtrl, onSend](wxCommandEvent&)
|
||||
{
|
||||
if (onSend(commentCtrl->GetValue()))
|
||||
{
|
||||
frame->Close(true);
|
||||
}
|
||||
});
|
||||
|
||||
buttonsLayout->Add(okButton);
|
||||
buttonsLayout->AddSpacer(5);
|
||||
buttonsLayout->Add(sendButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto okButton = new wxButton(frame, wxID_OK, wxT("OK"));
|
||||
okButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&)
|
||||
{
|
||||
frame->Close(true);
|
||||
});
|
||||
buttonsLayout->Add(okButton);
|
||||
}
|
||||
|
||||
mainLayout->Add(headerLayout, wxSizerFlags().Border(wxALL));
|
||||
if (onSend != nullptr)
|
||||
{
|
||||
mainLayout->AddSpacer(5);
|
||||
mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Click \"Send\" to submit the report to Audacity. This information is collected anonymously.")), wxSizerFlags().Border(wxALL));
|
||||
}
|
||||
mainLayout->AddSpacer(10);
|
||||
mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Problem details")), wxSizerFlags().Border(wxALL));
|
||||
|
||||
auto dumpTextCtrl = new wxTextCtrl(frame, wxID_ANY, dump, wxDefaultPosition, wxSize(500, 300), wxTE_RICH | wxTE_READONLY | wxTE_MULTILINE | wxTE_DONTWRAP);
|
||||
dumpTextCtrl->SetFont(wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)));
|
||||
dumpTextCtrl->ShowPosition(0);//scroll to top
|
||||
mainLayout->Add(dumpTextCtrl, wxSizerFlags().Border(wxALL).Expand());
|
||||
|
||||
if (onSend != nullptr)
|
||||
{
|
||||
mainLayout->AddSpacer(10);
|
||||
mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Comments")), wxSizerFlags().Border(wxALL));
|
||||
mainLayout->Add(commentCtrl, wxSizerFlags().Border(wxALL).Expand());
|
||||
}
|
||||
|
||||
mainLayout->Add(buttonsLayout, wxSizerFlags().Border(wxALL).Align(wxALIGN_RIGHT));
|
||||
frame->SetSizerAndFit(mainLayout);
|
||||
|
||||
frame->Show(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool CrashReportApp::OnInit()
|
||||
{
|
||||
if (!wxApp::OnInit())
|
||||
return false;
|
||||
|
||||
if (mSilent)
|
||||
{
|
||||
if (!mURL.empty())
|
||||
SendMinidump(mURL, mMinidumpPath, mArguments, wxEmptyString);
|
||||
}
|
||||
else
|
||||
{
|
||||
static std::unique_ptr<wxLocale> sLocale(new wxLocale(wxLANGUAGE_DEFAULT));
|
||||
#if defined(__WXOSX__)
|
||||
sLocale->AddCatalogLookupPathPrefix(wxT("../Resources"));
|
||||
#elif defined(__WXMSW__)
|
||||
sLocale->AddCatalogLookupPathPrefix(wxT("Languages"));
|
||||
#elif defined(__WXGTK__)
|
||||
sLocale->AddCatalogLookupPathPrefix(wxT("./locale"));
|
||||
sLocale->AddCatalogLookupPathPrefix(wxString::Format(wxT("%s/share/locale"), wxT(INSTALL_PREFIX)));
|
||||
#endif
|
||||
sLocale->AddCatalog("audacity");
|
||||
sLocale->AddCatalog("wxstd");
|
||||
|
||||
google_breakpad::Minidump minidump(mMinidumpPath.ToStdString(), false);
|
||||
if (minidump.Read())
|
||||
{
|
||||
SetExitOnFrameDelete(true);
|
||||
|
||||
wxFileName temp(mMinidumpPath);
|
||||
temp.SetExt("tmp");
|
||||
|
||||
try
|
||||
{
|
||||
ShowCrashReport(MakeHeaderString(minidump), MakeDumpString(minidump, temp.GetFullPath()));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
wxMessageBox(e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CrashReportApp::OnInitCmdLine(wxCmdLineParser& parser)
|
||||
{
|
||||
static const wxCmdLineEntryDesc cmdLineEntryDesc[] =
|
||||
{
|
||||
{ wxCMD_LINE_SWITCH, "h", "help", "Display help on the command line parameters", wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP },
|
||||
{ wxCMD_LINE_SWITCH, "s", "silent", "Send without displaying the confirmation dialog" },
|
||||
{ wxCMD_LINE_OPTION, "u", "url", "Crash report server URL", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
|
||||
{ wxCMD_LINE_OPTION, "a", "args", "A set of arguments to send", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
|
||||
{ wxCMD_LINE_PARAM, NULL, NULL, "path to minidump file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_OPTION_MANDATORY },
|
||||
{ wxCMD_LINE_NONE }
|
||||
};
|
||||
|
||||
parser.SetDesc(cmdLineEntryDesc);
|
||||
|
||||
wxApp::OnInitCmdLine(parser);
|
||||
}
|
||||
|
||||
bool CrashReportApp::OnCmdLineParsed(wxCmdLineParser& parser)
|
||||
{
|
||||
wxString url;
|
||||
wxString arguments;
|
||||
if (parser.Found("u", &url))
|
||||
{
|
||||
mURL = url.ToStdString();
|
||||
}
|
||||
if (parser.Found("a", &arguments))
|
||||
{
|
||||
try
|
||||
{
|
||||
mArguments = parseArguments(arguments.ToStdString());
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
wxMessageBox(e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
mMinidumpPath = parser.GetParam(0);
|
||||
mSilent = parser.Found("s");
|
||||
|
||||
return wxApp::OnCmdLineParsed(parser);
|
||||
}
|
||||
|
||||
void CrashReportApp::ShowCrashReport(const wxString& header, const wxString& text)
|
||||
{
|
||||
if (mURL.empty())
|
||||
{
|
||||
DoShowCrashReportFrame(header, text, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
DoShowCrashReportFrame(header, text, [this](const wxString& comments)
|
||||
{
|
||||
wxString commentsFilePath;
|
||||
if (!comments.empty())
|
||||
{
|
||||
wxFileName temp(mMinidumpPath);
|
||||
temp.SetName(temp.GetName() + "-comments");
|
||||
temp.SetExt("txt");
|
||||
commentsFilePath = temp.GetFullPath();
|
||||
wxFile file;
|
||||
if (file.Open(commentsFilePath, wxFile::write))
|
||||
{
|
||||
file.Write(comments);
|
||||
file.Close();
|
||||
}
|
||||
}
|
||||
|
||||
auto result = SendMinidump(mURL, mMinidumpPath, mArguments, commentsFilePath);
|
||||
if (!commentsFilePath.empty())
|
||||
wxRemoveFile(commentsFilePath);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
wxMessageBox(_("Failed to send crash report"));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
36
crashreports/crashreporter/CrashReportApp.h
Normal file
36
crashreports/crashreporter/CrashReportApp.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*!********************************************************************
|
||||
*
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
CrashReportApp.h
|
||||
|
||||
Vitaly Sverchinsky
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
//! Crash reporter GUI application
|
||||
/*! Used to send crash reports to a remote server, or view them.
|
||||
* Shows brief report content, and allows user to send report to developers.
|
||||
* Reporting URL and other parameters are specified as a command line arguments.
|
||||
*/
|
||||
class CrashReportApp final : public wxApp
|
||||
{
|
||||
std::string mURL;
|
||||
wxString mMinidumpPath;
|
||||
std::map<std::string, std::string> mArguments;
|
||||
|
||||
bool mSilent{ false };
|
||||
public:
|
||||
bool OnInit() override;
|
||||
void OnInitCmdLine(wxCmdLineParser& parser) override;
|
||||
bool OnCmdLineParsed(wxCmdLineParser& parser) override;
|
||||
|
||||
private:
|
||||
void ShowCrashReport(const wxString& header, const wxString& text);
|
||||
};
|
||||
|
||||
DECLARE_APP(CrashReportApp);
|
138
crashreports/crashreporter/warning.xpm
Normal file
138
crashreports/crashreporter/warning.xpm
Normal file
@@ -0,0 +1,138 @@
|
||||
/* XPM */
|
||||
static const char *warning[] = {
|
||||
/* columns rows colors chars-per-pixel */
|
||||
"24 24 108 2 ",
|
||||
" c None",
|
||||
". c black",
|
||||
"X c #010100",
|
||||
"o c #020200",
|
||||
"O c #020201",
|
||||
"+ c #070601",
|
||||
"@ c #070701",
|
||||
"# c #0E0C02",
|
||||
"$ c #151204",
|
||||
"% c #1C1804",
|
||||
"& c #1F1A05",
|
||||
"* c #261F00",
|
||||
"= c #262000",
|
||||
"- c #262005",
|
||||
"; c #292305",
|
||||
": c #2D2705",
|
||||
"> c #312A06",
|
||||
", c #403705",
|
||||
"< c #413806",
|
||||
"1 c #8F7300",
|
||||
"2 c #957700",
|
||||
"3 c #BA9500",
|
||||
"4 c #B99600",
|
||||
"5 c #E3B800",
|
||||
"6 c #E5BA00",
|
||||
"7 c #FEBB0B",
|
||||
"8 c #FFBC08",
|
||||
"9 c #FFBA0C",
|
||||
"0 c #FFBF10",
|
||||
"q c #FFBF11",
|
||||
"w c #FFC000",
|
||||
"e c #FFC100",
|
||||
"r c #FFC202",
|
||||
"t c #FFC400",
|
||||
"y c #FFC500",
|
||||
"u c #FFC700",
|
||||
"i c #FFC307",
|
||||
"p c #FFC800",
|
||||
"a c #FFC900",
|
||||
"s c #FECA00",
|
||||
"d c #FFCB00",
|
||||
"f c #FFCC00",
|
||||
"g c #FFCD00",
|
||||
"h c #FFCA04",
|
||||
"j c #FFCF04",
|
||||
"k c #FFC20B",
|
||||
"l c #FFC00D",
|
||||
"z c #FFC20D",
|
||||
"x c #FFC00E",
|
||||
"c c #FFCF0D",
|
||||
"v c #FED004",
|
||||
"b c #FFD104",
|
||||
"n c #FFD00D",
|
||||
"m c #FFD10D",
|
||||
"M c #FFD20D",
|
||||
"N c #FEC116",
|
||||
"B c #FFCB14",
|
||||
"V c #FFC61C",
|
||||
"C c #FFCA1F",
|
||||
"Z c #FFD215",
|
||||
"A c #FFD11C",
|
||||
"S c #FFD11D",
|
||||
"D c #FFD31D",
|
||||
"F c #FFD41D",
|
||||
"G c #E6C327",
|
||||
"H c #E7C527",
|
||||
"J c #FFC621",
|
||||
"K c #FFC525",
|
||||
"L c #FFC624",
|
||||
"P c #FFCC21",
|
||||
"I c #FFCA25",
|
||||
"U c #FFC927",
|
||||
"Y c #FFCB26",
|
||||
"T c #FFCC27",
|
||||
"R c #FFCE27",
|
||||
"E c #FFC22B",
|
||||
"W c #FFC52A",
|
||||
"Q c #FFC62C",
|
||||
"! c #F2CE29",
|
||||
"~ c #F3CF29",
|
||||
"^ c #FFC828",
|
||||
"/ c #FFCE28",
|
||||
"( c #FFCC2A",
|
||||
") c #FFCC2C",
|
||||
"_ c #FFD423",
|
||||
"` c #FFD623",
|
||||
"' c #FFD226",
|
||||
"] c #FFD527",
|
||||
"[ c #F6D129",
|
||||
"{ c #FFD129",
|
||||
"} c #FFD229",
|
||||
"| c #FFD22A",
|
||||
" . c #FFD32A",
|
||||
".. c #FDD42A",
|
||||
"X. c #FCD52B",
|
||||
"o. c #FFD42A",
|
||||
"O. c #FFD52A",
|
||||
"+. c #FFD52B",
|
||||
"@. c #FFD62A",
|
||||
"#. c #FFD62B",
|
||||
"$. c #FFD72B",
|
||||
"%. c #FFD828",
|
||||
"&. c #FFD92B",
|
||||
"*. c #FFDA2B",
|
||||
"=. c #FFDA2C",
|
||||
"-. c #FFC633",
|
||||
";. c #FFC830",
|
||||
":. c #FFCA31",
|
||||
/* pixels */
|
||||
" ",
|
||||
" -.E ",
|
||||
" / R ",
|
||||
" U +.+.) ",
|
||||
" ;.| +.+.| W ",
|
||||
" U +.[ [ +.R ",
|
||||
" ;.+.H X X G | J ",
|
||||
" R +.! X X ! +.R ",
|
||||
" L +.+.+.@ @ +.+.+.^ ",
|
||||
" | +.+.=.# # *.+.+.| ",
|
||||
" / +.+.+.*.$ $ *.+.+.+.U ",
|
||||
" L ' ] ] ] *.% % *.] ] ] ' Q ",
|
||||
" C ` ` ` ` ` - ; ` ` ` ` ` P ",
|
||||
" J D D D D D D : > F D D D D D V ",
|
||||
" B Z Z Z Z Z Z < , Z Z Z Z Z Z B ",
|
||||
" k c n n n n n n n n n n n n n n c x ",
|
||||
" q h j j j j j j j b b j j j j j j j h x ",
|
||||
" i f f f f f f f 6 = * 5 f f f f f f f w ",
|
||||
" 8 p p p p p p p p 3 X X 3 p p p f f p p p q ",
|
||||
" w p p p p p p p p p 2 1 p p p p p p p p p w ",
|
||||
"9 u u u u u u u u u u u u u u u u u u u u u u 7 ",
|
||||
"N u u u u u u u u u u u u u u u u u u u u u u x ",
|
||||
" ",
|
||||
" "
|
||||
};
|
Reference in New Issue
Block a user