435 lines
16 KiB
C++
435 lines
16 KiB
C++
/*
|
|
______________
|
|
//````````````\\ + by @ohsqueezy [http://ohsqueezy.itch.io] & @sleepin [http://instagram.com/sleepin]
|
|
//~~~~~~~~~~~~~~\\ + custom pudding code provided by Nuggets Select [http://nugget.fun]
|
|
//================\\ + available for copy, modification and redistribution [http://git.nugget.fun/pudding]
|
|
// \\
|
|
// ''CUSTOM PUDDING'' \\ 😀 Thank you for choosing Pudding Customs for your business 😀
|
|
//______________________\\
|
|
``````````````````````````
|
|
|
|
Generate a custom pudding from food product UPC codes and help a pair of rats take over the video game industry, using
|
|
their extraterrestrial ability to turn trash into performance enhancing drug puddings that enable business professionals
|
|
to predict the stock market with supernatural accuracy.
|
|
|
|
*/
|
|
|
|
#include "Pudding.hpp"
|
|
|
|
/* Launch the Pudding instance's mainloop */
|
|
int main()
|
|
{
|
|
Pudding pudding = Pudding();
|
|
pudding.run();
|
|
pudding.quit();
|
|
return 0;
|
|
}
|
|
|
|
/* Initialize a Pudding instance */
|
|
Pudding::Pudding()
|
|
{
|
|
/* subscribe to command events */
|
|
get_delegate().subscribe(&Pudding::respond, this);
|
|
/* initialize an opencv capture device for getting images from an attached camera */
|
|
int device_id = 0;
|
|
capture.open(device_id);
|
|
std::stringstream message;
|
|
if (capture.isOpened())
|
|
{
|
|
message << "opened and initialized " << capture.get(cv::CAP_PROP_FRAME_WIDTH) << "x" <<
|
|
capture.get(cv::CAP_PROP_FRAME_HEIGHT) << ", " << capture.get(cv::CAP_PROP_FPS) <<
|
|
"fps video capture device ID #" << device_id << " using " << capture.getBackendName();
|
|
}
|
|
else
|
|
{
|
|
message << "failed to open video capture device ID #" << device_id;
|
|
}
|
|
log(message.str());
|
|
/* initialize a zbar image scanner for reading barcodes of any format */
|
|
image_scanner.set_config(zbar::ZBAR_NONE, zbar::ZBAR_CFG_ENABLE, 1);
|
|
/* use sdl context for now */
|
|
load_sdl_context();
|
|
}
|
|
|
|
/* Respond to command events */
|
|
void Pudding::respond(SDL_Event& event)
|
|
{
|
|
if (get_delegate().compare(event, "up"))
|
|
{
|
|
increment_item_index();
|
|
}
|
|
else if (get_delegate().compare(event, "right"))
|
|
{
|
|
get_current_item().increment_image_index();
|
|
}
|
|
else if (get_delegate().compare(event, "down"))
|
|
{
|
|
increment_item_index(-1);
|
|
}
|
|
else if (get_delegate().compare(event, "left"))
|
|
{
|
|
get_current_item().increment_image_index(-1);
|
|
}
|
|
}
|
|
|
|
/* Build an Item object by submitting the upc parameter to multiple APIs and taking
|
|
* relevant results from each. Result JSON will be saved if saving is enabled in the global
|
|
* configuration
|
|
*/
|
|
void Pudding::add_item(const std::string& upc)
|
|
{
|
|
Item item(this);
|
|
item.set_upc(upc);
|
|
if (get_configuration()["api"]["open-food-enabled"])
|
|
{
|
|
incorporate_open_food_api(item);
|
|
}
|
|
if (get_configuration()["api"]["nutronix-enabled"])
|
|
{
|
|
incorporate_nutronix_api(item);
|
|
}
|
|
if (get_configuration()["api"]["edamam-enabled"])
|
|
{
|
|
incorporate_edamam_api(item);
|
|
}
|
|
if (get_configuration()["api"]["best-buy-enabled"])
|
|
{
|
|
incorporate_best_buy_api(item);
|
|
}
|
|
items.push_back(item);
|
|
increment_item_index();
|
|
}
|
|
|
|
/* Look for item upc in the Open Food API, and use the result to fill out item properties if found
|
|
*/
|
|
void Pudding::incorporate_open_food_api(Item& item)
|
|
{
|
|
log("checking Open Food API");
|
|
nlohmann::json json = json_from_url(OPEN_FOOD_API_URL + item.get_upc());
|
|
// test that should determine if an Open Food API response is not empty
|
|
if (json.value("status", 0) && json.contains("product"))
|
|
{
|
|
if (json["product"].value("image_url", "") != "")
|
|
{
|
|
std::string url = json["product"]["image_url"];
|
|
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
|
|
if (texture != nullptr)
|
|
{
|
|
item.add_image_texture(texture);
|
|
}
|
|
}
|
|
item.set_brand_name(json["product"].value("brands", ""));
|
|
item.set_product_name(json["product"].value("product_name", ""));
|
|
save_item_json(json, item, "Open_Food_API");
|
|
}
|
|
else
|
|
{
|
|
log("no results from Open Food API");
|
|
}
|
|
}
|
|
|
|
/* Look for item upc in the Nutronix API, and use the result to fill out item properties if found
|
|
*/
|
|
void Pudding::incorporate_nutronix_api(Item& item)
|
|
{
|
|
log("checking Nutronix API");
|
|
// Nutronix requires API keys in headers for validation
|
|
nlohmann::json json = json_from_url(
|
|
NUTRONIX_API_URL + item.get_upc(), {
|
|
"x-app-id: " + get_configuration()["api"]["nutronix-app-id"].get<std::string>(),
|
|
"x-app-key: " + get_configuration()["api"]["nutronix-app-key"].get<std::string>()
|
|
});
|
|
// test that should determine if a Nutronix response is not empty
|
|
if (!(json.contains("message") && json["message"] == NUTRONIX_NOT_FOUND))
|
|
{
|
|
nlohmann::json food = json["foods"][0];
|
|
if (food.contains("photo") && food["photo"].value("thumb", "") != "")
|
|
{
|
|
std::string url = food["photo"]["thumb"];
|
|
log("adding image listed in Nutronix API at " + url);
|
|
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
|
|
if (texture != nullptr)
|
|
{
|
|
item.add_image_texture(texture);
|
|
}
|
|
}
|
|
item.set_brand_name(food.value("brand_name", ""));
|
|
item.set_product_name(food.value("food_name", ""));
|
|
save_item_json(json, item, "Nutronix_API");
|
|
}
|
|
else
|
|
{
|
|
log("no results from Nutronix API");
|
|
}
|
|
}
|
|
|
|
/* Submit a query to Edamam API and insert relevant results into supplied Item object
|
|
*/
|
|
void Pudding::incorporate_edamam_api(Item& item)
|
|
{
|
|
log("checking Edamam API");
|
|
// build API url by concatenating relevant values into query string
|
|
std::stringstream url;
|
|
url << "https://api.edamam.com/api/food-database/v2/parser?upc=" << item.get_upc() << "&app_id=" <<
|
|
get_configuration()["api"]["edamam-app-id"].get<std::string>() << "&app_key=" <<
|
|
get_configuration()["api"]["edamam-app-key"].get<std::string>();
|
|
nlohmann::json json = json_from_url(url.str());
|
|
// test that should determine if a Edamam response has food data
|
|
if (json.contains("hints") && json["hints"][0].contains("food"))
|
|
{
|
|
nlohmann::json food = json["hints"][0]["food"];
|
|
if (food.value("image", "") != "")
|
|
{
|
|
std::string url = food["image"];
|
|
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
|
|
if (texture != nullptr)
|
|
{
|
|
item.add_image_texture(texture);
|
|
}
|
|
item.set_product_name(food.value("label", ""));
|
|
}
|
|
save_item_json(json, item, "Edamam_API");
|
|
}
|
|
}
|
|
|
|
/* Submit a query to the Best Buy API and insert relevant results into supplied Item object
|
|
*/
|
|
void Pudding::incorporate_best_buy_api(Item& item)
|
|
{
|
|
log("checking Best Buy API");
|
|
// build API url by concatenating relevant values into query string
|
|
std::stringstream url;
|
|
url << "https://api.bestbuy.com/v1/products(upc=" << item.get_upc() << ")?format=json&apiKey=" <<
|
|
get_configuration()["api"]["best-buy-api-key"].get<std::string>();
|
|
nlohmann::json json = json_from_url(url.str());
|
|
// test that should determine if a Best Buy response has a result
|
|
if (json.contains("total") && json["total"].get<int>() > 0)
|
|
{
|
|
nlohmann::json product = json["products"][0];
|
|
// look up image (for games this is box art) and "alternate views image" (for games this is a screen shot)
|
|
for (std::string key : {"alternateViewsImage", "image"})
|
|
{
|
|
if (product.value(key, "") != "")
|
|
{
|
|
std::string url = product[key];
|
|
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
|
|
if (texture != nullptr)
|
|
{
|
|
item.add_image_texture(texture);
|
|
}
|
|
}
|
|
}
|
|
item.set_product_name(product.value("name", ""));
|
|
save_item_json(json, item, "Best_Buy_API");
|
|
}
|
|
}
|
|
|
|
/* Write submitted JSON to file, creating parent directories if necessary, and using item and
|
|
* api_name to determine file name prefix
|
|
*/
|
|
void Pudding::save_item_json(const nlohmann::json& json, const Item& item, const std::string& api_name) const
|
|
{
|
|
if (get_configuration()["scan"]["json-save"])
|
|
{
|
|
fs::path path = get_configuration()["scan"]["json-save-directory"];
|
|
if (!fs::exists(path))
|
|
{
|
|
fs::create_directories(path);
|
|
}
|
|
std::string prefix = api_name;
|
|
if (item.get_full_name() != "")
|
|
{
|
|
prefix += "_" + item.get_full_name();
|
|
}
|
|
else
|
|
{
|
|
prefix += "_Unknown";
|
|
}
|
|
std::replace_if(prefix.begin(), prefix.end(), [](char c) { return !std::isalnum(c); }, '_');
|
|
path /= prefix + "_" + item.get_upc() + ".json";
|
|
std::ofstream out(path);
|
|
out << std::setw(4) << json << std::endl;
|
|
log("Saved JSON to " + path.string());
|
|
}
|
|
else
|
|
{
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_CUSTOM, "not saving JSON, saving disabled by configuration");
|
|
}
|
|
}
|
|
|
|
/* Download the JSON data at the submitted URL, and return it as a JSON object
|
|
*/
|
|
nlohmann::json Pudding::json_from_url(const std::string& url, const std::vector<std::string>& headers)
|
|
{
|
|
std::vector<std::uint8_t> storage;
|
|
curl_get_bytes(url, storage, headers);
|
|
nlohmann::json json = nlohmann::json::parse(storage);
|
|
std::stringstream json_formatted;
|
|
json_formatted << std::setw(4) << json << std::endl;
|
|
debug(json_formatted.str());
|
|
return json;
|
|
}
|
|
|
|
/* Store the byte buffer from the submitted URL downloaded by cURL into the supplied storage vector
|
|
*/
|
|
void Pudding::curl_get_bytes(const std::string& url, std::vector<std::uint8_t>& storage, const std::vector<std::string>& headers)
|
|
{
|
|
CURL *curl;
|
|
CURLcode result;
|
|
result = curl_global_init(CURL_GLOBAL_DEFAULT);
|
|
if (result != CURLE_OK)
|
|
{
|
|
std::cout << "curl initialization failed " << curl_easy_strerror(result) << std::endl;
|
|
}
|
|
else
|
|
{
|
|
curl = curl_easy_init();
|
|
if (curl)
|
|
{
|
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Pudding::curl_write_response);
|
|
std::vector<std::uint8_t> food_barcode_response;
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &storage);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, get_configuration()["api"]["user-agent"].get<std::string>().c_str());
|
|
struct curl_slist* list = nullptr;
|
|
if (headers.size() > 0)
|
|
{
|
|
for (const std::string& header : headers)
|
|
{
|
|
list = curl_slist_append(list, header.c_str());
|
|
}
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
|
|
result = curl_easy_perform(curl);
|
|
curl_slist_free_all(list);
|
|
if (result != CURLE_OK)
|
|
{
|
|
std::cout << "curl request failed " << curl_easy_strerror(result) << std::endl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cout << "curl initialization failed" << std::endl;
|
|
}
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
curl_global_cleanup();
|
|
}
|
|
|
|
/* This callback will be called by cURL when it has a response char buffer. The chars will be inserted into the storage
|
|
* vector pointed to by the storage parameter.
|
|
*/
|
|
size_t Pudding::curl_write_response(std::uint8_t* buffer, size_t size, size_t count, std::vector<std::uint8_t>* storage)
|
|
{
|
|
size_t total_size = size * count;
|
|
storage->insert(storage->end(), buffer, buffer + total_size);
|
|
return total_size;
|
|
}
|
|
|
|
/* Get an image at the submitted URL as a pointer to SDL_Texture memory
|
|
*/
|
|
std::shared_ptr<SDL_Texture> Pudding::texture_from_image_url(const std::string& url)
|
|
{
|
|
log("looking up image at " + url);
|
|
std::vector<std::uint8_t> storage;
|
|
curl_get_bytes(url, storage);
|
|
if (!storage.empty())
|
|
{
|
|
SDL_RWops* rw = SDL_RWFromConstMem(storage.data(), storage.size());
|
|
debug("received image data");
|
|
return std::shared_ptr<SDL_Texture>(IMG_LoadTexture_RW(get_renderer(), rw, 0), Pudding::destroy_texture);
|
|
}
|
|
else
|
|
{
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_CUSTOM, "image url returned no data");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
/* Call SDL's destroy texture function, and print a debug statement for testing. This is defined as a static member
|
|
* function and uses the SDL function instead of the inherited logging functions from Node since the object may not
|
|
* be allocated at destruction time (?)
|
|
*/
|
|
void Pudding::destroy_texture(SDL_Texture* texture)
|
|
{
|
|
// not sure why SDL_Log works here but SDL_LogDebug and SDL_LogInfo don't
|
|
SDL_Log("destroying texture %p", texture);
|
|
SDL_DestroyTexture(texture);
|
|
}
|
|
|
|
/* Change the currently selected item */
|
|
void Pudding::increment_item_index(int increment)
|
|
{
|
|
current_item_index = sfw::mod(current_item_index + increment, static_cast<int>(items.size()));
|
|
}
|
|
|
|
Item& Pudding::get_current_item()
|
|
{
|
|
return items[current_item_index];
|
|
}
|
|
|
|
/* Update parameters and draw the screen */
|
|
void Pudding::update()
|
|
{
|
|
/* reload the config file every frame to check for changes */
|
|
get_root()->configuration.load("config.json");
|
|
get_root()->configuration.merge();
|
|
if (current_config_barcode != get_configuration()["scan"]["barcode"])
|
|
{
|
|
current_config_barcode = get_configuration()["scan"]["barcode"];
|
|
current_barcode = current_config_barcode;
|
|
std::stringstream message;
|
|
message << "read new barcode from config " << current_barcode;
|
|
log(message.str());
|
|
}
|
|
/* draw the current item image to the left half of the screen if items are available */
|
|
Box video_box = get_window_box();
|
|
if (items.size() > 0)
|
|
{
|
|
Box item_box = Box({0.0f, 0.0f}, {get_window_box().get_w() / 2.0f, get_window_box().get_h()});
|
|
SDL_RenderCopyF(get_renderer(), get_current_item().get_active_image_texture().get(), nullptr, &item_box);
|
|
video_box.set_left(get_window_box().get_center_x(), true);
|
|
}
|
|
/* draw the camera to the right half of the screen if the camera has been opened */
|
|
if (capture.isOpened())
|
|
{
|
|
capture.read(capture_frame);
|
|
if (!capture_frame.empty())
|
|
{
|
|
/* convert opencv matrix to sdl texture */
|
|
SDL_Texture* texture = SDL_CreateTexture(
|
|
get_renderer(), SDL_PIXELFORMAT_BGR24, SDL_TEXTUREACCESS_STATIC, capture_frame.cols, capture_frame.rows);
|
|
SDL_UpdateTexture(texture, nullptr, static_cast<void*>(capture_frame.data), capture_frame.step1());
|
|
SDL_RenderCopyF(get_renderer(), texture, nullptr, &video_box);
|
|
SDL_DestroyTexture(texture);
|
|
/* scan with zbar */
|
|
cv::Mat gray;
|
|
cv::cvtColor(capture_frame, gray, cv::COLOR_BGR2GRAY);
|
|
zbar::Image query_image(gray.cols, gray.rows, "Y800", static_cast<void*>(gray.data), gray.cols * gray.rows);
|
|
int result = image_scanner.scan(query_image);
|
|
if (result > 0)
|
|
{
|
|
for (zbar::Image::SymbolIterator symbol = query_image.symbol_begin(); symbol != query_image.symbol_end(); ++symbol)
|
|
{
|
|
std::stringstream message;
|
|
message << "camera scanned " << symbol->get_type_name() << " symbol " << symbol->get_data();
|
|
log(message.str());
|
|
current_camera_barcode = symbol->get_data();
|
|
current_barcode = current_camera_barcode;
|
|
}
|
|
}
|
|
query_image.set_data(nullptr, 0);
|
|
}
|
|
else
|
|
{
|
|
debug("video capture device frame empty");
|
|
}
|
|
}
|
|
/* add a new item if a new barcode was scanned or entered */
|
|
if (current_barcode != previous_barcode)
|
|
{
|
|
add_item(current_barcode);
|
|
previous_barcode = current_barcode;
|
|
}
|
|
}
|