Merge pull request #46 from alfred-reynolds/reddit_rss_reader

Add configuration file support along with other features
This commit is contained in:
Manuel Dewald
2021-11-15 10:42:49 +01:00
committed by GitHub
20 changed files with 1319 additions and 312 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ make
.git
build
.vscode
test_config/slide.options.json

103
README.md
View File

@@ -6,6 +6,7 @@ Tested versions:
* Raspberry Pi 3 running Raspbian Stretch.
* Raspberry Pi 3 running Raspbian Buster.
* Raspberry Pi Zero running Raspbian Buster.
* Raspberry Pi 4B running Raspbian Buster.
Screen background is filled with a scaled version of the image to prevent pure black background.
@@ -17,20 +18,25 @@ This project is maintained by myself during my spare time. If you like and use i
## Usage
```
slide [-t rotation_seconds] [-T transition_seconds] [-c/--overlay-color overlay_color(#rrggbb)] [-a aspect] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-O overlay_string] [-v] [-s] [-S] [--verbose] [--stretch]
slide [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color overlay_color(#rrggbb)] [-a aspect] [-o background_opacity(0..255)] [-b blur_radius] [-p image_folder|-i imageFile,...] [-r] [-O overlay_string] [-v] [--verbose] [--stretch] [-c path_to_config_json]
```
* `image_folder`: where to search for images (.jpg files)
* `-i imageFile,...`: comma delimited list of full paths to image files to display
* `-c path_to_config_json`: the path to an optional slide.options.json file containing configuration parameters
* `-t` how many seconds to display each picture for
* `-r` for recursive traversal of `image_folder`
* `-s` for shuffle instead of random image rotation
* `-S` for sorted rotation (files ordered by name, first images then subfolders)
* `rotation_seconds(default=30)`: time until next random image is chosen from the given folder
* `aspect(default=a)`: the required aspect ratio of the picture to display. Valid values are 'a' (all), 'l' (landscape), 'p' (portrait) and 'm' (monitor). Monitor will match the aspect ratio of the display we are running on.
* `transition_seconds(default=1)`: time of image transition animation. Default is 1 second, and transition animation will be disabled if the value is set to 0
* `aspect(default=a)`: the required aspect ratio of the picture to display. Valid values are 'a' (all), 'l' (landscape) and 'p' (portrait)
* `background_opacity(default=150)`: opacity of the background filling image between 0 (black background) and 255
* `blur_radius(default=20)`: blur radius of the background filling image
* `-v` or `--verbose`: Verbose debug output when running, plus a thumbnail of the original image in the bottom left of the screen
* `--stretch`: When in aspect mode 'l' or 'p' crop the image rather than leaving a blurred background. For example, in landscape mode this will make images as wide as the screen and crop the top and bottom to fit.
* `--stretch`: When in aspect mode 'l','p' or 'm' crop the image rather than leaving a blurred background. For example, in landscape mode this will make images as wide as the screen and crop the top and bottom to fit.
* `-h` or `--overlay-color` the color of the overlay text, in the form of 3 or 6 digits hex rgb string prefixed by `#`, for example `#00FF00` or `#0F0` for color 🟢
* `-O` is used to create a overlay string.
* It defines overlays for all four edges in the order `top-left;top-right;bottom-left;bottom-right`
* All edges overlays are separated by `;`
@@ -46,9 +52,100 @@ slide [-t rotation_seconds] [-T transition_seconds] [-c/--overlay-color overlay_
* `<dir>`directory of the current image
* `<path>`path to the current image without filename
* Example: `slide -p ./images -O "20|60|Time: <time>;;;Picture taken at <exifdatetime>"`
* `-c` or `--overlay-color` the color of the overlay text, in the form of 3 or 6 digits hex rgb string prefixed by `#`, for example `#00FF00` or `#0F0` for color 🟢
To exit the application, press escape. If you're using a touch display, touch all 4 corners at the same time.
## Configuration file
Slide supports loading configuration from a JSON formatted file called `slide.options.json`. This file can be specified by the `-c` command line option, we will also attempt to read `~/.config/slide/slide.options.json` and `/etc/slide/slide.options.json` in that order. The first file to load is used and its options will override command line parameters.
The file format is:
```
{
"path" : "/path/to/pictures",
"aspect" : "m",
"overlay" : "20|20|<filename>",
"shuffle" : true,
"recursive" : true,
"sorted" : false,
"stretch": false,
"rotationSeconds" : 300,
"opacity" : 200,
"debug" : false,
"scheduler" : [
{
"exclusive" : true,
"path" : "/path/to/pictures/reddit_sync"
"stretch" : true,
"times": [
{
"start": "14:00",
"end": "16:00"
}
]
},
{
"exclusive" : true,
"stretch" : false,
"times": [
{
"start": "08:00",
"end": "10:00"
},
{
"start": "16:00",
"end": "19:00"
}
],
"path" : "/path/to/pictures/show_peak_times/"
},
{
"path" : "/path/to/pictures/always_show_1"
},
{
"path" : "/path/to/pictures/always_show_2"
}
}
```
Supported keys and values in the JSON configuration are:
* `path` : where to search for images (.jpg files). This path is ignored if the `scheduler` feature is used.
* `aspect` : the same as the command line argument
* `overlay` : the same as the overlay command line argument
* `shuffle` : set to true to enable shuffle mode for file display
* `recursive` : set to true to enable recursive mode for file display
* `sorted` : set to true to enable shuffle mode for file display
* `stretch` : set to true to enable, the same as the `--stretch` command line argument
* `rotationSeconds` : the same as the `-t` command line argument
* `opacity` : the same as the command line `-o` argument
* `blur` : the same as the command line `-b` argument
* `debug` : set to true to enable verbose output from the program
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
* `times` : times is a JSON array of start and end times in which it is valid to display this image. The time is in the format HH:MM:SS and is based on the systems local time. If `start` isn't defined then it defaults to the start of the day, if `end` isn't defined it defaults to the end of the day.
* `path` : the path to image files
* `stretch` : as above
## Folder Options file
When using the default or recursive folder mode we support having per folder display options. The options are stored in a file called "options.json" in the images folder and support a subset of the applications configuration settings:
```
{
"stretch": false,
"aspect" : "m",
"opacity" : 200,
"blur" : 20,
"times": [
{
"start": "08:00",
"end": "10:00"
},
{
"start": "17:00",
"end": "19:00"
}
]
}
```
See the `Configuration File` section for details of each setting.
## Dependencies
* qt5-qmake

296
src/appconfig.cpp Normal file
View File

@@ -0,0 +1,296 @@
#include "appconfig.h"
#include "logger.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QDateTime>
#include <QTime>
#include <QFileInfo>
#include <QDir>
#include <iostream>
const std::string AppConfig::valid_aspects = "alpm"; // all, landscape, portait, monitor
ImageAspectScreenFilter parseAspectFromString(char aspect) {
switch(aspect)
{
case 'l':
return ImageAspectScreenFilter_Landscape;
break;
case 'p':
return ImageAspectScreenFilter_Portrait;
break;
case 'm':
return ImageAspectScreenFilter_Monitor;
break;
default:
case 'a':
return ImageAspectScreenFilter_Any;
break;
}
}
std::string ParseJSONString(QJsonObject jsonDoc, const char *key) {
if(jsonDoc.contains(key) && jsonDoc[key].isString())
{
return jsonDoc[key].toString().toStdString();
}
return "";
}
void SetJSONBool(bool &value, QJsonObject jsonDoc, const char *key) {
if(jsonDoc.contains(key) && jsonDoc[key].isBool())
{
value = jsonDoc[key].toBool();
}
}
Config loadConfiguration(const std::string &configFilePath, const Config &currentConfig) {
if(configFilePath.empty())
{
return currentConfig;
}
QString jsonFile(configFilePath.c_str());
QDir directory;
if(!directory.exists(jsonFile))
{
return currentConfig; // nothing to load
}
Config userConfig = currentConfig;
Log( "Found options file: ", jsonFile.toStdString() );
QString val;
QFile file;
file.setFileName(jsonFile);
file.open(QIODevice::ReadOnly | QIODevice::Text);
val = file.readAll();
file.close();
QJsonDocument d = QJsonDocument::fromJson(val.toUtf8());
QJsonObject jsonDoc = d.object();
SetJSONBool(userConfig.baseDisplayOptions.fitAspectAxisToWindow, jsonDoc, "stretch");
std::string aspectString = ParseJSONString(jsonDoc, "aspect");
if(!aspectString.empty())
{
userConfig.baseDisplayOptions.onlyAspect = parseAspectFromString(aspectString[0]);
}
if(jsonDoc.contains("rotationSeconds") && jsonDoc["rotationSeconds"].isDouble())
{
userConfig.rotationSeconds = (int)jsonDoc["rotationSeconds"].toDouble();
}
if(jsonDoc.contains("opacity") && jsonDoc["opacity"].isDouble())
{
userConfig.backgroundOpacity = (int)jsonDoc["opacity"].toDouble();
}
if(jsonDoc.contains("blur") && jsonDoc["blur"].isDouble())
{
userConfig.blurRadius = (int)jsonDoc["blur"].toDouble();
}
if(jsonDoc.contains("times") && jsonDoc["times"].isArray())
{
QJsonArray jsonArray = jsonDoc["times"].toArray();
foreach (const QJsonValue & value, jsonArray)
{
QJsonObject obj = value.toObject();
if(obj.contains("start") || obj.contains("end"))
{
DisplayTimeWindow window;
if(obj.contains("start"))
{
window.startDisplay = QTime::fromString(obj["start"].toString());
}
if(obj.contains("end"))
{
window.endDisplay = QTime::fromString(obj["end"].toString());
}
userConfig.baseDisplayOptions.timeWindows.append(window);
}
}
}
userConfig.loadTime = QDateTime::currentDateTime();
return userConfig;
}
AppConfig loadConfiguration(const std::string &configFilePath, const AppConfig &currentConfig) {
AppConfig userConfig = currentConfig;
// make sure to only update the base members, preserve the ones from the copy above
(Config &)userConfig = loadConfiguration(configFilePath, (const Config &)userConfig);
return userConfig;
}
QString getAppConfigFilePath(const std::string &configPath) {
std::string userConfigFolder = "~/.config/slide/";
std::string systemConfigFolder = "/etc/slide";
QString baseConfigFilename("slide.options.json");
QDir directory(userConfigFolder.c_str());
QString jsonFile = "";
if (!configPath.empty())
{
directory.setPath(configPath.c_str());
jsonFile = directory.filePath(baseConfigFilename);
}
if(!directory.exists(jsonFile))
{
directory.setPath(userConfigFolder.c_str());
jsonFile = directory.filePath(baseConfigFilename);
}
if(!directory.exists(jsonFile))
{
directory.setPath(systemConfigFolder.c_str());
jsonFile = directory.filePath(baseConfigFilename);
}
if(directory.exists(jsonFile))
{
return jsonFile;
}
return "";
}
QVector<PathEntry> parsePathEntry(QJsonObject &jsonMainDoc, bool baseRecursive, bool baseShuffle, bool baseSorted)
{
QVector<PathEntry> pathEntries;
if(jsonMainDoc.contains("scheduler") && jsonMainDoc["scheduler"].isArray())
{
QJsonArray jsonArray = jsonMainDoc["scheduler"].toArray();
foreach (const QJsonValue & value, jsonArray)
{
PathEntry entry;
entry.recursive = baseRecursive;
entry.sorted = baseSorted;
entry.shuffle = baseShuffle;
QJsonObject schedulerJson = value.toObject();
SetJSONBool(entry.recursive, schedulerJson, "recursive");
SetJSONBool(entry.shuffle, schedulerJson, "shuffle");
SetJSONBool(entry.sorted, schedulerJson, "sorted");
SetJSONBool(entry.baseDisplayOptions.fitAspectAxisToWindow, schedulerJson, "stretch");
std::string pathString = ParseJSONString(schedulerJson, "path");
if(!pathString.empty()) {
entry.path = pathString;
}
std::string imageListString = ParseJSONString(schedulerJson, "imageList");
if(!imageListString.empty()) {
entry.imageList = imageListString;
}
SetJSONBool(entry.exclusive, schedulerJson, "exclusive");
if(schedulerJson.contains("times") && schedulerJson["times"].isArray())
{
QJsonArray jsonTimesArray = schedulerJson["times"].toArray();
foreach (const QJsonValue & timesValue, jsonTimesArray)
{
QJsonObject timesJson = timesValue.toObject();
if(timesJson.contains("start") || timesJson.contains("end"))
{
DisplayTimeWindow window;
if(timesJson.contains("start"))
{
window.startDisplay = QTime::fromString(timesJson["start"].toString());
}
if(timesJson.contains("end"))
{
window.endDisplay = QTime::fromString(timesJson["end"].toString());
}
entry.baseDisplayOptions.timeWindows.append(window);
}
}
}
pathEntries.append(entry);
}
}
return pathEntries;
}
AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
if(commandLineConfig.configPath.empty())
{
return commandLineConfig;
}
QString jsonFile = getAppConfigFilePath(commandLineConfig.configPath);
QDir directory;
if(!directory.exists(jsonFile))
{
return commandLineConfig;
}
AppConfig loadedConfig = loadConfiguration(jsonFile.toStdString(), commandLineConfig);
QString val;
QFile file;
file.setFileName(jsonFile);
file.open(QIODevice::ReadOnly | QIODevice::Text);
val = file.readAll();
file.close();
QJsonDocument d = QJsonDocument::fromJson(val.toUtf8());
QJsonObject jsonDoc = d.object();
bool baseRecursive = false, baseShuffle = false, baseSorted = false;
SetJSONBool(baseRecursive, jsonDoc, "recursive");
SetJSONBool(baseShuffle, jsonDoc, "shuffle");
SetJSONBool(baseSorted, jsonDoc, "sorted");
SetJSONBool(loadedConfig.debugMode, jsonDoc, "debug");
std::string overlayString = ParseJSONString(jsonDoc, "overlay");
if(!overlayString.empty())
{
loadedConfig.overlay = overlayString;
}
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
if(loadedConfig.paths.count() <= 0)
{
PathEntry entry;
entry.recursive = baseRecursive;
entry.sorted = baseSorted;
entry.shuffle = baseShuffle;
std::string pathString = ParseJSONString(jsonDoc, "path");
if(!pathString.empty())
{
entry.path = pathString;
}
std::string imageListString = ParseJSONString(jsonDoc, "imageList");
if(!imageListString.empty())
{
entry.imageList = imageListString;
}
loadedConfig.paths.append(entry);
}
loadedConfig.configPath = commandLineConfig.configPath;
return loadedConfig;
}
Config getConfigurationForFolder(const std::string &folderPath, const Config &currentConfig) {
if(folderPath.empty())
{
return currentConfig;
}
QDir directory(folderPath.c_str());
QString jsonFile = directory.filePath(QString("options.json"));
if(directory.exists(jsonFile))
{
return loadConfiguration(jsonFile.toStdString(), currentConfig );
}
return currentConfig;
}

86
src/appconfig.h Normal file
View File

@@ -0,0 +1,86 @@
#ifndef APPCONFIG_H
#define APPCONFIG_H
#include <QDateTime>
#include "imagestructs.h"
#include <QVector>
// configuration options that apply to an image/folder of images
struct Config {
public:
unsigned int rotationSeconds = 30;
unsigned int transitionTime = 1;
int blurRadius = -1;
int backgroundOpacity = -1;
ImageDisplayOptions baseDisplayOptions;
QDateTime loadTime;
};
struct PathEntry {
std::string path = "";
std::string imageList = "";
bool exclusive = false; // only use this entry when it is valid, skip others
bool recursive = false;
bool shuffle = false;
bool sorted = false;
ImageDisplayOptions baseDisplayOptions;
bool operator==(const PathEntry &b) const
{
return !operator!=(b);
}
bool operator!=(const PathEntry &b) const
{
if (b.exclusive != exclusive)
return true;
if (b.recursive != recursive || b.shuffle != shuffle || b.sorted != sorted)
return true;
if(b.baseDisplayOptions.fitAspectAxisToWindow != baseDisplayOptions.fitAspectAxisToWindow)
return true;
if (b.path != path || b.imageList != imageList)
return true;
if (b.baseDisplayOptions.timeWindows.count() != baseDisplayOptions.timeWindows.count())
return true;
for(int i = 0; i < baseDisplayOptions.timeWindows.count(); ++i)
{
if (b.baseDisplayOptions.timeWindows[i] != baseDisplayOptions.timeWindows[i])
return true;
}
return false;
}
};
// app level configuration
struct AppConfig : public Config {
AppConfig() {}
AppConfig( const Config &inConfig ) : Config(inConfig) {}
std::string configPath = "";
std::string overlay = "";
QString overlayHexRGB = "#FFFFFF";
QVector<PathEntry> paths;
bool debugMode = false;
static const std::string valid_aspects;
public:
bool PathOptionsChanged(AppConfig &other)
{
if (paths.count() != other.paths.count())
return true;
for(int index = 0; index < paths.count(); ++index)
{
if(other.paths[index] != paths[index])
return true;
}
return false;
}
};
AppConfig loadAppConfiguration(const AppConfig &commandLineConfig);
Config getConfigurationForFolder(const std::string &folderPath, const Config &currentConfig);
ImageAspectScreenFilter parseAspectFromString(char aspect);
QString getAppConfigFilePath(const std::string &configPath);
#endif

View File

@@ -1,6 +1,7 @@
#include "imageselector.h"
#include "pathtraverser.h"
#include "mainwindow.h"
#include "logger.h"
#include <QDirIterator>
#include <QTimer>
#include <QApplication>
@@ -12,26 +13,55 @@
#include <algorithm> // std::shuffle
#include <random> // std::default_random_engine
ImageSelector::ImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspectIn):
pathTraverser(pathTraverser), aspect(aspectIn)
ImageSelector::ImageSelector(std::unique_ptr<PathTraverser>& pathTraverserIn):
pathTraverser(std::move(pathTraverserIn))
{
}
ImageSelector::ImageSelector() {}
ImageSelector::~ImageSelector(){}
int ImageSelector::getImageRotation(const std::string& fileName)
int ReadExifTag(ExifData* exifData, ExifTag tag, bool shortRead = false)
{
int orientation = 0;
ExifData *exifData = exif_data_new_from_file(fileName.c_str());
if (exifData)
{
int value = -1;
ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
ExifEntry *exifEntry = exif_data_get_entry(exifData, EXIF_TAG_ORIENTATION);
ExifEntry *exifEntry = exif_data_get_entry(exifData, tag);
if (exifEntry)
{
orientation = exif_get_short(exifEntry->data, byteOrder);
if (shortRead)
{
return exif_get_short(exifEntry->data, byteOrder);
}
return exif_get_long(exifEntry->data, byteOrder);
}
return value;
}
ImageDetails ImageSelector::populateImageDetails(const std::string&fileName, const ImageDisplayOptions &baseOptions)
{
ImageDetails imageDetails;
int orientation = -1;
int imageWidth = -1;
int imageHeight = -1;
ExifData *exifData = exif_data_new_from_file(fileName.c_str());
if (exifData)
{
orientation = ReadExifTag(exifData, EXIF_TAG_ORIENTATION, true);
/*
// It looks like you can't trust Exif dimensions, so just forcefully load the file below
// try to get the image dimensions from exifData so we don't need to fully load the file
imageWidth = ReadExifTag(exifData, EXIF_TAG_IMAGE_WIDTH);
if ( imageWidth == -1)
imageWidth = ReadExifTag(exifData, EXIF_TAG_PIXEL_X_DIMENSION);
imageHeight = ReadExifTag(exifData, EXIF_TAG_RELATED_IMAGE_WIDTH); // means height, height is related to width
if ( imageHeight == -1)
imageHeight = ReadExifTag(exifData, EXIF_TAG_PIXEL_Y_DIMENSION);*/
exif_data_free(exifData);
}
@@ -46,101 +76,125 @@ int ImageSelector::getImageRotation(const std::string& fileName)
case 6:
degrees = 90;
break;
default:
break;
}
return degrees;
}
bool ImageSelector::imageMatchesFilter(const std::string& fileName)
{
if(!QFileInfo::exists(QString(fileName.c_str())))
if (imageWidth <=0 || imageHeight <=0)
{
if(debugMode)
{
std::cout << "file not found: " << fileName << std::endl;
}
return false;
}
if(!imageValidForAspect(fileName)) {
if(debugMode)
{
std::cout << "image aspect ratio doesn't match filter '" << aspect << "' : " << fileName << std::endl;
}
return false;
}
return true;
}
bool ImageSelector::imageValidForAspect(const std::string& fileName)
{
// fallback to QPixmap to determine image size
QPixmap p( fileName.c_str() );
int imageWidth = p.width();
int imageHeight = p.height();
int rotation = getImageRotation(fileName);
if ( rotation == 90 || rotation == 270 )
imageWidth = p.width();
imageHeight = p.height();
}
// if the image is rotated then swap height/width here to show displayed sizes
if( degrees == 90 || degrees == 270 )
{
std::swap(imageWidth,imageHeight);
}
switch(aspect)
// setup the imageDetails structure
imageDetails.filename = fileName;
imageDetails.width = imageWidth;
imageDetails.height = imageHeight;
imageDetails.rotation = degrees;
imageDetails.options = pathTraverser->UpdateOptionsForImage(imageDetails.filename, baseOptions);
return imageDetails;
}
bool ImageSelector::imageInsideTimeWindow(const QVector<DisplayTimeWindow> &timeWindows)
{
if(timeWindows.count() == 0)
{
case 'a':
// allow all
break;
case 'l':
if ( imageWidth < imageHeight )
return true; // no specified time windows means always display
}
const QTime currentTime = QTime::currentTime();
for(auto &window : timeWindows)
{
if(currentTime > window.startDisplay && currentTime < window.endDisplay)
{
return true;
}
}
if(ShouldLog() && timeWindows.count() > 0)
{
Log( "image display time outside window: ");
for(auto &timeWindow : timeWindows)
{
Log("time: ", timeWindow.startDisplay.toString().toStdString(), "-", timeWindow.endDisplay.toString().toStdString());
}
}
return false;
}
bool ImageSelector::imageMatchesFilter(const ImageDetails& imageDetails)
{
if(!QFileInfo::exists(QString(imageDetails.filename.c_str())))
{
Log("file not found: ", imageDetails.filename);
return false;
}
break;
case 'p':
if ( imageHeight < imageWidth )
if(!imageValidForAspect(imageDetails))
{
Log("image aspect ratio doesn't match filter '", imageDetails.options.onlyAspect, "' : ", imageDetails.filename);
return false;
}
break;
if(!imageInsideTimeWindow(imageDetails.options.timeWindows))
{
return false;
}
return true;
}
bool ImageSelector::imageValidForAspect(const ImageDetails& imageDetails)
{
if (imageDetails.isValidForScreenAspect(imageDetails.options.onlyAspect))
{
return true;
}
return false;
}
RandomImageSelector::RandomImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect):
ImageSelector(pathTraverser, aspect)
RandomImageSelector::RandomImageSelector(std::unique_ptr<PathTraverser>& pathTraverser):
ImageSelector(pathTraverser)
{
srand (time(NULL));
}
RandomImageSelector::~RandomImageSelector(){}
std::string RandomImageSelector::getNextImage()
const ImageDetails RandomImageSelector::getNextImage(const ImageDisplayOptions &baseOptions)
{
std:: string filename;
ImageDetails imageDetails;
try
{
QStringList images = pathTraverser->getImages();
unsigned int selectedImage = selectRandom(images);
filename = pathTraverser->getImagePath(images.at(selectedImage).toStdString());
while(!imageMatchesFilter(filename))
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(selectedImage).toStdString()), baseOptions);
while(!imageMatchesFilter(imageDetails))
{
unsigned int selectedImage = selectRandom(images);
filename = pathTraverser->getImagePath(images.at(selectedImage).toStdString());
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(selectedImage).toStdString()), baseOptions);
}
}
catch(const std::string& err)
{
std::cerr << "Error: " << err << std::endl;
}
std::cout << "updating image: " << filename << std::endl;
return filename;
std::cout << "updating image: " << imageDetails.filename << std::endl;
return imageDetails;
}
unsigned int RandomImageSelector::selectRandom(const QStringList& images) const
{
if(debugMode)
{
std::cout << "images: " << images.size() << std::endl;
}
Log("images: ", images.size());
if (images.size() == 0)
{
throw std::string("No jpg images found in given folder");
@@ -148,8 +202,8 @@ unsigned int RandomImageSelector::selectRandom(const QStringList& images) const
return rand() % images.size();
}
ShuffleImageSelector::ShuffleImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect):
ImageSelector(pathTraverser, aspect),
ShuffleImageSelector::ShuffleImageSelector(std::unique_ptr<PathTraverser>& pathTraverser):
ImageSelector(pathTraverser),
current_image_shuffle(-1),
images()
{
@@ -160,22 +214,30 @@ ShuffleImageSelector::~ShuffleImageSelector()
{
}
std::string ShuffleImageSelector::getNextImage()
const ImageDetails ShuffleImageSelector::getNextImage(const ImageDisplayOptions &baseOptions)
{
reloadImagesIfNoneLeft();
ImageDetails imageDetails;
if (images.size() == 0)
{
return "";
return imageDetails;
}
std::string filename = pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString());
bool bReloadedImages = false;
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()), baseOptions);
current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image
while(!imageMatchesFilter(filename)) {
while(!imageMatchesFilter(imageDetails)) {
if(current_image_shuffle >= images.size()) {
// don't keep looping
if(bReloadedImages == true)
return ImageDetails();
bReloadedImages = true;
}
reloadImagesIfNoneLeft();
std::string filename = pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString());
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()),baseOptions);
current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image
}
std::cout << "updating image: " << filename << std::endl;
return filename;
std::cout << "updating image: " << imageDetails.filename << std::endl;
return imageDetails;
}
void ShuffleImageSelector::reloadImagesIfNoneLeft()
@@ -191,8 +253,8 @@ void ShuffleImageSelector::reloadImagesIfNoneLeft()
}
}
SortedImageSelector::SortedImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect):
ImageSelector(pathTraverser, aspect),
SortedImageSelector::SortedImageSelector(std::unique_ptr<PathTraverser>& pathTraverser):
ImageSelector(pathTraverser),
images()
{
srand (time(NULL));
@@ -214,21 +276,31 @@ bool operator<(const QString& lhs, const QString& rhs) noexcept{
}
std::string SortedImageSelector::getNextImage()
const ImageDetails SortedImageSelector::getNextImage(const ImageDisplayOptions &baseOptions)
{
reloadImagesIfEmpty();
ImageDetails imageDetails;
if (images.size() == 0)
{
return "";
return imageDetails;
}
std::string filename = pathTraverser->getImagePath(images.takeFirst().toStdString());
while(!imageMatchesFilter(filename)) {
reloadImagesIfEmpty();
filename = pathTraverser->getImagePath(images.takeFirst().toStdString());
bool bReloadedImages = false;
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions);
while(!imageMatchesFilter(imageDetails)) {
if (images.size() == 0) {
// don't keep looping
if(bReloadedImages == true)
return ImageDetails();
bReloadedImages = true;
}
std::cout << "updating image: " << filename << std::endl;
return filename;
reloadImagesIfEmpty();
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions);
}
std::cout << "updating image: " << imageDetails.filename << std::endl;
imageDetails.options = pathTraverser->UpdateOptionsForImage(imageDetails.filename, imageDetails.options);
return imageDetails;
}
void SortedImageSelector::reloadImagesIfEmpty()
@@ -237,12 +309,66 @@ void SortedImageSelector::reloadImagesIfEmpty()
{
images = pathTraverser->getImages();
std::sort(images.begin(), images.end());
if(debugMode)
if(ShouldLog())
{
std::cout << "read " << images.size() << " images." << std::endl;
Log( "read ", images.size(), " images.");
for (int i = 0;i <images.size();i++){
std::cout << images[i].toStdString() << std::endl;
Log(images[i].toStdString());
}
}
}
}
ListImageSelector::ListImageSelector()
{
currentSelector = imageSelectors.begin();
}
ListImageSelector::~ListImageSelector()
{
}
void ListImageSelector::AddImageSelector(std::unique_ptr<ImageSelector>& selector, const bool exclusiveIn, const ImageDisplayOptions& baseDisplayOptionsIn)
{
SelectoryEntry entry;
entry.selector = std::move(selector);
entry.exclusive = exclusiveIn;
entry.baseDisplayOptions = baseDisplayOptionsIn;
imageSelectors.push_back(std::move(entry));
currentSelector = imageSelectors.begin();
}
const ImageDetails ListImageSelector::getNextImage(const ImageDisplayOptions& baseOptions)
{
// check for exclusive time windows
for(auto& selector: imageSelectors)
{
if (imageInsideTimeWindow(selector.baseDisplayOptions.timeWindows) && selector.exclusive)
{
ImageDisplayOptions options = baseOptions;
if (selector.baseDisplayOptions.fitAspectAxisToWindow)
options.fitAspectAxisToWindow = true;
return selector.selector->getNextImage(options);
}
}
// fall back to the next in the list
do
{
++currentSelector;
if(currentSelector == imageSelectors.end())
{
currentSelector = imageSelectors.begin();
}
if (imageInsideTimeWindow(currentSelector->baseDisplayOptions.timeWindows))
{
ImageDisplayOptions options = baseOptions;
if (currentSelector->baseDisplayOptions.fitAspectAxisToWindow)
options.fitAspectAxisToWindow = true;
return currentSelector->selector->getNextImage(options);
}
}
while(true);
}

View File

@@ -4,6 +4,8 @@
#include <iostream>
#include <memory>
#include <QStringList>
#include <QVector>
#include "imagestructs.h"
class MainWindow;
class PathTraverser;
@@ -11,26 +13,25 @@ class PathTraverser;
class ImageSelector
{
public:
ImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspectIn);
ImageSelector(std::unique_ptr<PathTraverser>& pathTraverser);
ImageSelector(); // use case for when you don't own your own traverser
virtual ~ImageSelector();
virtual std::string getNextImage() = 0;
void setDebugMode(bool debugModeIn) { debugMode = debugModeIn;}
virtual const ImageDetails getNextImage(const ImageDisplayOptions &baseOptions) = 0;
protected:
int getImageRotation(const std::string& fileName);
bool imageMatchesFilter(const std::string& fileName);
bool imageValidForAspect(const std::string& fileName);
std::unique_ptr<PathTraverser>& pathTraverser;
char aspect;
bool debugMode = false;
ImageDetails populateImageDetails(const std::string&filename, const ImageDisplayOptions &baseOptions);
bool imageValidForAspect(const ImageDetails& imageDetails);
bool imageMatchesFilter(const ImageDetails& imageDetails);
bool imageInsideTimeWindow(const QVector<DisplayTimeWindow> &timeWindows);
std::unique_ptr<PathTraverser> pathTraverser;
};
class RandomImageSelector : public ImageSelector
{
public:
RandomImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect);
RandomImageSelector(std::unique_ptr<PathTraverser>& pathTraverser);
virtual ~RandomImageSelector();
virtual std::string getNextImage();
virtual const ImageDetails getNextImage(const ImageDisplayOptions &baseOptions);
private:
unsigned int selectRandom(const QStringList& images) const;
@@ -39,9 +40,9 @@ private:
class ShuffleImageSelector : public ImageSelector
{
public:
ShuffleImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect);
ShuffleImageSelector(std::unique_ptr<PathTraverser>& pathTraverser);
virtual ~ShuffleImageSelector();
virtual std::string getNextImage();
virtual const ImageDetails getNextImage(const ImageDisplayOptions &baseOptions);
private:
void reloadImagesIfNoneLeft();
@@ -52,12 +53,30 @@ private:
class SortedImageSelector : public ImageSelector
{
public:
SortedImageSelector(std::unique_ptr<PathTraverser>& pathTraverser, char aspect);
SortedImageSelector(std::unique_ptr<PathTraverser>& pathTraverser);
virtual ~SortedImageSelector();
virtual std::string getNextImage();
virtual const ImageDetails getNextImage(const ImageDisplayOptions &baseOptions);
private:
void reloadImagesIfEmpty();
QStringList images;
};
class ListImageSelector : public ImageSelector
{
public:
ListImageSelector();
virtual ~ListImageSelector();
virtual const ImageDetails getNextImage(const ImageDisplayOptions &baseOptions);
void AddImageSelector(std::unique_ptr<ImageSelector>& selector, const bool exclusiveIn, const ImageDisplayOptions& baseDisplayOptionsIn);
private:
struct SelectoryEntry {
std::unique_ptr<ImageSelector> selector;
ImageDisplayOptions baseDisplayOptions;
bool exclusive = false;
};
std::vector<SelectoryEntry> imageSelectors;
std::vector<SelectoryEntry>::iterator currentSelector;
};
#endif // IMAGESELECTOR_H

54
src/imagestructs.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "imagestructs.h"
#include "logger.h"
ImageDetails::ImageDetails()
{
}
ImageDetails::~ImageDetails()
{
}
ImageAspect ImageDetails::aspect() const
{
if (width > height)
{
return ImageAspect_Landscape;
}
else if (height > width)
{
return ImageAspect_Portrait;
}
// must be square, so just pick something
return ImageAspect_Portrait;
}
bool ImageDetails::isValidForScreenAspect(const ImageAspectScreenFilter aspectScreen) const
{
if(aspectScreen == ImageAspectScreenFilter_Any)
{
return true;
}
if(aspectScreen == ImageAspectScreenFilter_Monitor)
{
Log("Error: invalid aspect of ImageAspectScreenFilter_Monitor" );
return false;
}
if (width > height)
{
return aspectScreen == ImageAspectScreenFilter_Landscape;
}
else if (height > width)
{
return aspectScreen == ImageAspectScreenFilter_Portrait;
}
// must be square so let is always display
return true;
}

49
src/imagestructs.h Normal file
View File

@@ -0,0 +1,49 @@
#ifndef IMAGESTRUCTS_H
#define IMAGESTRUCTS_H
#include <QTime>
#include <QVector>
#include <string>
// possible aspect ratios of an image
enum ImageAspect { ImageAspect_Landscape = 0, ImageAspect_Portrait};
enum ImageAspectScreenFilter { ImageAspectScreenFilter_Landscape = 0, ImageAspectScreenFilter_Portrait, ImageAspectScreenFilter_Any, ImageAspectScreenFilter_Monitor /* match monitors aspect */ };
struct DisplayTimeWindow
{
QTime startDisplay = QTime(0,0,0,0);
QTime endDisplay = QTime(23,59,59,0);
bool operator!=(const DisplayTimeWindow &b) const
{
return startDisplay != b.startDisplay || endDisplay != b.endDisplay;
}
};
// options to consider when displaying an image
struct ImageDisplayOptions
{
ImageAspectScreenFilter onlyAspect = ImageAspectScreenFilter_Any;
bool fitAspectAxisToWindow = false;
QVector<DisplayTimeWindow> timeWindows;
};
// details of a particular image
class ImageDetails
{
public:
ImageDetails();
~ImageDetails();
bool isValidForScreenAspect(const ImageAspectScreenFilter aspectDisplay) const;
ImageAspect aspect() const;
public:
int width = 0;
int height = 0;
int rotation = 0;
std::string filename;
ImageDisplayOptions options;
};
#endif // IMAGESTRUCTS_H

View File

@@ -9,11 +9,11 @@
#include <stdlib.h> /* srand, rand */
#include <time.h> /* time */
ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeout, std::unique_ptr<ImageSelector>& selector):
ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeoutMsec, std::unique_ptr<ImageSelector>& selector):
QObject::QObject(),
window(w),
timeout(timeout),
selector(selector),
timeout(timeoutMsec),
selector(std::move(selector)),
timer(this),
timerNoContent(this)
{
@@ -21,15 +21,19 @@ ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeout, std::unique_pt
void ImageSwitcher::updateImage()
{
std::string filename(selector->getNextImage());
if (filename == "")
if(reloadConfigIfNeeded)
{
reloadConfigIfNeeded(window, this);
}
ImageDetails imageDetails = selector->getNextImage(window.getBaseOptions());
if (imageDetails.filename == "")
{
window.warn("No image found.");
timerNoContent.start(timeoutNoContent);
}
else
{
window.setImage(filename);
window.setImage(imageDetails);
timerNoContent.stop(); // we have loaded content so stop the fast polling
}
}
@@ -41,3 +45,25 @@ void ImageSwitcher::start()
connect(&timerNoContent, SIGNAL(timeout()), this, SLOT(updateImage()));
timer.start(timeout);
}
void ImageSwitcher::scheduleImageUpdate()
{
// update our image in 100msec, to let the system settle
QTimer::singleShot(100, this, SLOT(updateImage()));
}
void ImageSwitcher::setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn)
{
reloadConfigIfNeeded = reloadConfigIfNeededIn;
}
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
{
timeout = timeoutMsecIn;
timer.start(timeout);
}
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
{
selector = std::move(selectorIn);
}

View File

@@ -5,25 +5,31 @@
#include <QTimer>
#include <iostream>
#include <memory>
#include <functional>
#include "imageselector.h"
class MainWindow;
class ImageSelector;
class ImageSwitcher : public QObject
{
Q_OBJECT
public:
ImageSwitcher(MainWindow& w, unsigned int timeout, std::unique_ptr<ImageSelector>& selector);
ImageSwitcher(MainWindow& w, unsigned int timeoutMsec, std::unique_ptr<ImageSelector>& selector);
void start();
void scheduleImageUpdate();
void setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn);
void setRotationTime(unsigned int timeoutMsec);
void setImageSelector(std::unique_ptr<ImageSelector>& selector);
public slots:
void updateImage();
private:
MainWindow& window;
unsigned int timeout;
std::unique_ptr<ImageSelector>& selector;
std::unique_ptr<ImageSelector> selector;
QTimer timer;
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
QTimer timerNoContent;
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
};
#endif // IMAGESWITCHER_H

14
src/logger.cpp Normal file
View File

@@ -0,0 +1,14 @@
#include "logger.h"
static bool shouldLog = false;
void SetupLogger(bool shouldLogIn)
{
shouldLog = shouldLogIn;
}
bool ShouldLog()
{
return shouldLog;
}

20
src/logger.h Normal file
View File

@@ -0,0 +1,20 @@
#ifndef LOGGER_H
#define LOGGER_H
#include <iostream>
#include <string_view>
#include <sstream>
void SetupLogger(bool shouldLog);
bool ShouldLog();
template <typename ...Args>
void Log(Args&& ...args) {
if(!ShouldLog())
return;
std::ostringstream stream;
(stream << ... << std::forward<Args>(args)) << std::endl;
std::cout << stream.str();
}
#endif // LOGGER_H

View File

@@ -3,6 +3,9 @@
#include "imageswitcher.h"
#include "pathtraverser.h"
#include "overlay.h"
#include "appconfig.h"
#include "logger.h"
#include <QApplication>
#include <QRegularExpression>
#include <iostream>
@@ -16,162 +19,257 @@
#include <memory>
void usage(std::string programName) {
std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-T transition_seconds] [-c/--overlay-color #rrggbb] [-a aspect('l','p','a')] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-O overlay_string] [-s] [-S] [-v] [--verbose] [--stretch]" << std::endl;
std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color #rrggbb] [-a aspect('l','p','a', 'm')] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-s] [-S] [-v] [--verbose] [--stretch] [-c config_file_path]" << std::endl;
}
int main(int argc, char *argv[])
{
unsigned int rotationSeconds = 30;
std::string path = "";
QApplication a(argc, argv);
MainWindow w;
bool parseCommandLine(AppConfig &appConfig, int argc, char *argv[]) {
int opt;
bool recursive = false;
bool shuffle = false;
bool sorted = false;
bool debugMode = false;
char aspect = 'a';
bool fitAspectAxisToWindow = false;
std::string valid_aspects = "alp"; // all, landscape, portait
std::string overlay = "";
QString overlayHexRGB = QString();
QRegularExpression hexRGBMatcher("^#([0-9A-Fa-f]{3}){1,2}$");
int debugInt = 0;
int stretchInt = 0;
static struct option long_options[] =
{
{"verbose", no_argument, &debugInt, 1},
{"stretch", no_argument, &stretchInt, 1},
{"overlay-color", required_argument, 0, 'c'},
{"overlay-color", required_argument, 0, 'h'},
};
int option_index = 0;
while ((opt = getopt_long(argc, argv, "b:p:t:T:o:O:c:a:rsSv", long_options, &option_index)) != -1) {
while ((opt = getopt_long(argc, argv, "b:p:t:T:o:O:a:i:c:h:rsSv", long_options, &option_index)) != -1) {
switch (opt) {
case 0:
/* If this option set a flag, do nothing else now. */
if (long_options[option_index].flag != 0)
break;
usage(argv[0]);
return 1;
return false;
break;
case 'p':
path = optarg;
if(appConfig.paths.count() == 0)
appConfig.paths.append(PathEntry());
appConfig.paths[0].path = optarg;
break;
case 'a':
aspect = optarg[0];
if ( valid_aspects.find(aspect) == std::string::npos )
if (appConfig.valid_aspects.find(optarg[0]) == std::string::npos)
{
std::cout << "Invalid Aspect option, defaulting to all" << std::endl;
aspect = 'a';
appConfig.baseDisplayOptions.onlyAspect = ImageAspectScreenFilter_Any;
}
else
{
appConfig.baseDisplayOptions.onlyAspect = parseAspectFromString(optarg[0]);
}
break;
case 't':
rotationSeconds = atoi(optarg);
appConfig.rotationSeconds = atoi(optarg);
break;
case 'T':
w.setTransitionTime(atoi(optarg));
appConfig.transitionTime =atoi(optarg);
break;
case 'b':
w.setBlurRadius(atoi(optarg));
appConfig.blurRadius = atoi(optarg);
break;
case 'o':
w.setBackgroundOpacity(atoi(optarg));
appConfig.backgroundOpacity = atoi(optarg);
break;
case 'r':
recursive = true;
if(appConfig.paths.count() == 0)
appConfig.paths.append(PathEntry());
appConfig.paths[0].recursive = true;
break;
case 's':
shuffle = true;
if(appConfig.paths.count() == 0)
appConfig.paths.append(PathEntry());
appConfig.paths[0].shuffle = true;
std::cout << "Shuffle mode is on." << std::endl;
break;
case 'S':
sorted = true;
if(appConfig.paths.count() == 0)
appConfig.paths.append(PathEntry());
appConfig.paths[0].sorted = true;
break;
case 'O':
overlay = optarg;
appConfig.overlay = optarg;
break;
case 'c':
overlayHexRGB = QString::fromStdString(optarg);
case 'h':
appConfig.overlayHexRGB = QString::fromStdString(optarg);
break;
case 'v':
debugMode = true;
appConfig.debugMode = true;
break;
case 'i':
if(appConfig.paths.count() == 0)
appConfig.paths.append(PathEntry());
appConfig.paths[0].imageList = optarg;
break;
case 'c':
appConfig.configPath = optarg;
break;
default: /* '?' */
usage(argv[0]);
return 1;
return false;
}
}
if(debugInt==1)
{
debugMode = true;
appConfig.debugMode = true;
}
if(stretchInt==1)
{
fitAspectAxisToWindow = true;
appConfig.baseDisplayOptions.fitAspectAxisToWindow = true;
}
if (path.empty())
return true;
}
void ConfigureWindowFromSettings(MainWindow &w, const AppConfig &appConfig)
{
if (appConfig.blurRadius>= 0)
{
w.setBlurRadius(appConfig.blurRadius);
}
if (appConfig.backgroundOpacity>= 0)
{
w.setBackgroundOpacity(appConfig.backgroundOpacity);
}
w.setTransitionTime(appConfig.transitionTime);
if (!appConfig.overlayHexRGB.isEmpty())
{
QRegularExpression hexRGBMatcher("^#([0-9A-Fa-f]{3}){1,2}$");
if(!hexRGBMatcher.match(appConfig.overlayHexRGB).hasMatch())
{
std::cout << "Error: hex rgb string expected. e.g. #FFFFFF or #FFF" << std::endl;
}
else
{
w.setOverlayHexRGB(appConfig.overlayHexRGB);
}
}
if (!appConfig.overlay.empty())
{
std::unique_ptr<Overlay> o = std::unique_ptr<Overlay>(new Overlay(appConfig.overlay));
w.setOverlay(o);
}
w.setBaseOptions(appConfig.baseDisplayOptions);
}
std::unique_ptr<ImageSelector> GetSelectorForConfig(const PathEntry& path)
{
std::unique_ptr<PathTraverser> pathTraverser;
if (!path.imageList.empty())
{
pathTraverser = std::unique_ptr<PathTraverser>(new ImageListPathTraverser(path.imageList));
}
else if (path.recursive)
{
pathTraverser = std::unique_ptr<PathTraverser>(new RecursivePathTraverser(path.path));
}
else
{
pathTraverser = std::unique_ptr<PathTraverser>(new DefaultPathTraverser(path.path));
}
std::unique_ptr<ImageSelector> selector;
if (path.sorted)
{
selector = std::unique_ptr<ImageSelector>(new SortedImageSelector(pathTraverser));
}
else if (path.shuffle)
{
selector = std::unique_ptr<ImageSelector>(new ShuffleImageSelector(pathTraverser));
}
else
{
selector = std::unique_ptr<ImageSelector>(new RandomImageSelector(pathTraverser));
}
return selector;
}
std::unique_ptr<ImageSelector> GetSelectorForApp(const AppConfig& appConfig)
{
if(appConfig.paths.count()==1)
{
return GetSelectorForConfig(appConfig.paths[0]);
}
else
{
std::unique_ptr<ListImageSelector> listSelector(new ListImageSelector());
for(const auto &path : appConfig.paths)
{
auto selector = GetSelectorForConfig(path);
listSelector->AddImageSelector(selector, path.exclusive, path.baseDisplayOptions);
}
// new things
return listSelector;
}
}
void ReloadConfigIfNeeded(AppConfig &appConfig, MainWindow &w, ImageSwitcher *switcher)
{
if(appConfig.configPath.empty())
{
return;
}
QString jsonFile = getAppConfigFilePath(appConfig.configPath);
QDir directory;
if(!directory.exists(jsonFile))
{
return;
}
if(appConfig.loadTime < QFileInfo(jsonFile).lastModified())
{
AppConfig oldConfig = appConfig;
appConfig = loadAppConfiguration(appConfig);
ConfigureWindowFromSettings(w, appConfig);
if(appConfig.PathOptionsChanged(oldConfig))
{
std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig);
switcher->setImageSelector(selector);
}
switcher->setRotationTime(appConfig.rotationSeconds * 1000);
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
AppConfig commandLineAppConfig;
if (!parseCommandLine(commandLineAppConfig, argc, argv))
{
usage(argv[0]);
return 1;
}
AppConfig appConfig = loadAppConfiguration(commandLineAppConfig);
if (appConfig.paths.empty())
{
std::cout << "Error: Path expected." << std::endl;
usage(argv[0]);
return 1;
}
SetupLogger(appConfig.debugMode);
Log( "Rotation Time: ", appConfig.rotationSeconds );
Log( "Overlay input: ", appConfig.overlay );
if (!overlayHexRGB.isEmpty())
{
if(!hexRGBMatcher.match(overlayHexRGB).hasMatch())
{
std::cout << "Error: hex rgb string expected. e.g. #FFFFFF or #FFF" << std::endl;
return 1;
}
w.setOverlayHexRGB(overlayHexRGB);
}
std::unique_ptr<PathTraverser> pathTraverser;
if (recursive)
{
pathTraverser = std::unique_ptr<PathTraverser>(new RecursivePathTraverser(path));
}
else
{
pathTraverser = std::unique_ptr<PathTraverser>(new DefaultPathTraverser(path));
}
std::unique_ptr<ImageSelector> selector;
if (sorted)
{
selector = std::unique_ptr<ImageSelector>(new SortedImageSelector(pathTraverser, aspect));
}
else if (shuffle)
{
selector = std::unique_ptr<ImageSelector>(new ShuffleImageSelector(pathTraverser, aspect));
}
else
{
selector = std::unique_ptr<ImageSelector>(new RandomImageSelector(pathTraverser, aspect));
}
selector->setDebugMode(debugMode);
if(debugMode)
{
std::cout << "Rotation Time: " << rotationSeconds << std::endl;
std::cout << "Overlay input: " << overlay << std::endl;
}
Overlay o(overlay);
o.setDebugMode(debugMode);
if (!overlay.empty())
{
w.setOverlay(&o);
}
w.setAspect(aspect);
w.setDebugMode(debugMode);
w.setFitAspectAxisToWindow(fitAspectAxisToWindow);
MainWindow w;
ConfigureWindowFromSettings(w, appConfig);
w.show();
ImageSwitcher switcher(w, rotationSeconds * 1000, selector);
std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig);
ImageSwitcher switcher(w, appConfig.rotationSeconds * 1000, selector);
w.setImageSwitcher(&switcher);
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
switcher.setConfigFileReloader(reloader);
switcher.start();
return a.exec();
}

View File

@@ -1,6 +1,8 @@
#include "mainwindow.h"
#include "overlay.h"
#include "ui_mainwindow.h"
#include "imageswitcher.h"
#include "logger.h"
#include <QLabel>
#include <QPixmap>
#include <QBitmap>
@@ -14,7 +16,8 @@
#include <QRect>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
#include <sstream>
#include <QApplication>
#include <QScreen>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
@@ -32,6 +35,16 @@ MainWindow::MainWindow(QWidget *parent) :
label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
update();
QScreen* screen = QGuiApplication::primaryScreen();
if (screen)
{
connect(screen, SIGNAL(geometryChanged(QRect)), this, SLOT(checkWindowSize()));
connect(screen, SIGNAL(orientationChanged(Qt::ScreenOrientation)), this, SLOT(checkWindowSize()));
screen->setOrientationUpdateMask(Qt::LandscapeOrientation |
Qt::PortraitOrientation |
Qt::InvertedLandscapeOrientation |
Qt::InvertedPortraitOrientation);
}
}
MainWindow::~MainWindow()
@@ -109,50 +122,51 @@ void MainWindow::resizeEvent(QResizeEvent* event)
updateImage();
}
void MainWindow::setImage(std::string path)
void MainWindow::checkWindowSize()
{
currentImage = path;
QScreen *screen = QGuiApplication::primaryScreen();
if (screen != nullptr)
{
QSize screenSize = screen->geometry().size();
if(size() != screenSize)
{
Log("Resizing Window", screenSize.width(), "," , screenSize.height() );
setFixedSize(screenSize);
updateImage();
}
if (imageAspectMatchesMonitor)
{
bool isLandscape = screenSize.width() > screenSize.height();
ImageAspectScreenFilter newAspect = isLandscape ? ImageAspectScreenFilter_Landscape : ImageAspectScreenFilter_Portrait;
if (newAspect != baseImageOptions.onlyAspect)
{
Log("Changing image orientation to ", newAspect);
baseImageOptions.onlyAspect = newAspect;
currentImage.filename = "";
warn("Monitor aspect changed, updating image...");
repaint(); // force an immediate redraw as we might block for a while loading the next image
if (switcher != nullptr)
{
// pick a new image as our aspect changed, we can't just resize the image
switcher->scheduleImageUpdate();
}
}
}
}
}
int MainWindow::getImageRotation()
void MainWindow::setImage(const ImageDetails &imageDetails)
{
if (currentImage == "")
return 0;
int orientation = 0;
ExifData *exifData = exif_data_new_from_file(currentImage.c_str());
if (exifData)
{
ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
ExifEntry *exifEntry = exif_data_get_entry(exifData, EXIF_TAG_ORIENTATION);
if (exifEntry)
{
orientation = exif_get_short(exifEntry->data, byteOrder);
}
exif_data_free(exifData);
}
int degrees = 0;
switch(orientation) {
case 8:
degrees = 270;
break;
case 3:
degrees = 180;
break;
case 6:
degrees = 90;
break;
}
return degrees;
currentImage = imageDetails;
updateImage();
}
void MainWindow::updateImage()
{
if (currentImage == "")
checkWindowSize();
if (currentImage.filename == "")
return;
QLabel *label = this->findChild<QLabel*>("image");
@@ -164,24 +178,23 @@ void MainWindow::updateImage()
this->setPalette(palette);
}
QPixmap p( currentImage.c_str() );
if(debugMode)
{
std::cout << "size:" << p.width() << "x" << p.height() << std::endl;
}
QPixmap p;
p.load( currentImage.filename.c_str() );
Log("size:", p.width(), "x", p.height(), "(window:", width(), ",", height(), ")");
QPixmap rotated = getRotatedPixmap(p);
QPixmap scaled = getScaledPixmap(rotated);
QPixmap background = getBlurredBackground(rotated, scaled);
drawForeground(background, scaled);
if (overlay != NULL)
if (overlay != nullptr)
{
drawText(background, overlay->getMarginTopLeft(), overlay->getFontsizeTopLeft(), overlay->getRenderedTopLeft(currentImage).c_str(), Qt::AlignTop|Qt::AlignLeft);
drawText(background, overlay->getMarginTopRight(), overlay->getFontsizeTopRight(), overlay->getRenderedTopRight(currentImage).c_str(), Qt::AlignTop|Qt::AlignRight);
drawText(background, overlay->getMarginBottomLeft(), overlay->getFontsizeBottomLeft(), overlay->getRenderedBottomLeft(currentImage).c_str(), Qt::AlignBottom|Qt::AlignLeft);
drawText(background, overlay->getMarginBottomRight(), overlay->getFontsizeBottomRight(), overlay->getRenderedBottomRight(currentImage).c_str(), Qt::AlignBottom|Qt::AlignRight);
if (debugMode)
drawText(background, overlay->getMarginTopLeft(), overlay->getFontsizeTopLeft(), overlay->getRenderedTopLeft(currentImage.filename).c_str(), Qt::AlignTop|Qt::AlignLeft);
drawText(background, overlay->getMarginTopRight(), overlay->getFontsizeTopRight(), overlay->getRenderedTopRight(currentImage.filename).c_str(), Qt::AlignTop|Qt::AlignRight);
drawText(background, overlay->getMarginBottomLeft(), overlay->getFontsizeBottomLeft(), overlay->getRenderedBottomLeft(currentImage.filename).c_str(), Qt::AlignBottom|Qt::AlignLeft);
drawText(background, overlay->getMarginBottomRight(), overlay->getFontsizeBottomRight(), overlay->getRenderedBottomRight(currentImage.filename).c_str(), Qt::AlignBottom|Qt::AlignRight);
if (ShouldLog())
{
// draw a thumbnail version of the source image in the bottom left, to check for cropping issues
QPainter pt(&background);
@@ -216,7 +229,6 @@ void MainWindow::updateImage()
}
void MainWindow::drawText(QPixmap& image, int margin, int fontsize, QString text, int alignment) {
//std::cout << "text: " << text.toStdString() << " margin: " << margin << " fontsize: " << fontsize<< std::endl;
QPainter pt(&image);
pt.setPen(QPen(QColor(overlayHexRGB)));
pt.setFont(QFont("Sans", fontsize, QFont::Bold));
@@ -235,21 +247,18 @@ void MainWindow::drawForeground(QPixmap& background, const QPixmap& foreground)
pt.drawPixmap((background.width()-foreground.width())/2, (background.height()-foreground.height())/2, foreground);
}
void MainWindow::setOverlay(Overlay* o)
void MainWindow::setOverlay(std::unique_ptr<Overlay> &o)
{
overlay = o;
}
void MainWindow::setAspect(char aspectIn)
{
aspect = aspectIn;
overlay = std::move(o);
}
QPixmap MainWindow::getBlurredBackground(const QPixmap& originalSize, const QPixmap& scaled)
{
if (fitAspectAxisToWindow) {
// our scaled version will just fill the whole screen, us it directly
return scaled.copy();
if (currentImage.options.fitAspectAxisToWindow) {
// our scaled version will just fill the whole screen, use it directly
//Log("Using scaled image");
QRect rect((scaled.width() - width())/2, 0, width(), height());
return scaled.copy(rect);
} else if (scaled.width() < width()) {
QPixmap background = blur(originalSize.scaledToWidth(width(), Qt::SmoothTransformation));
QRect rect(0, (background.height() - height())/2, width(), height());
@@ -265,26 +274,40 @@ QPixmap MainWindow::getBlurredBackground(const QPixmap& originalSize, const QPix
QPixmap MainWindow::getRotatedPixmap(const QPixmap& p)
{
QMatrix matrix;
matrix.rotate(getImageRotation());
matrix.rotate(currentImage.rotation);
return p.transformed(matrix);
}
QPixmap MainWindow::getScaledPixmap(const QPixmap& p)
{
if (fitAspectAxisToWindow)
if (currentImage.options.fitAspectAxisToWindow)
{
if ( aspect == 'p')
bool stretchWidth = currentImage.aspect() == ImageAspect_Landscape;
bool stretchHeight = currentImage.aspect() == ImageAspect_Portrait;
// check the stretched image will naturally fill the screen for its aspect ratio
if (stretchHeight && (width() > ((double)height()/p.height())*p.width()))
{
// stretched via height won't fill the width, so stretch the other way
stretchHeight = false;
stretchWidth = true;
}
else if (stretchWidth && (height() > ((double)width()/p.width())*p.height()))
{
// stretched via width won't fill the width, so stretch the other way
stretchWidth = false;
stretchHeight = true;
}
if (stretchHeight)
{
// potrait mode, make height of image fit screen and crop top/bottom
QPixmap pTemp = p.scaledToHeight(height(), Qt::SmoothTransformation);
return pTemp.copy(0,0,width(),height());
}
else if ( aspect == 'l')
else if (stretchWidth)
{
// landscape mode, make width of image fit screen and crop top/bottom
QPixmap pTemp = p.scaledToWidth(width(), Qt::SmoothTransformation);
//int imageTempWidth = pTemp.width();
//int imageTempHeight = pTemp.height();
return pTemp.copy(0,0,width(),height());
}
}
@@ -341,6 +364,7 @@ void MainWindow::setBackgroundOpacity(unsigned int backgroundOpacity)
void MainWindow::setOverlayHexRGB(QString overlayHexRGB)
{
this->overlayHexRGB = overlayHexRGB;
}
void MainWindow::setTransitionTime(unsigned int transitionSeconds)
{
@@ -352,3 +376,23 @@ void MainWindow::warn(std::string text)
QLabel *label = this->findChild<QLabel*>("image");
label->setText(text.c_str());
}
void MainWindow::setBaseOptions(const ImageDisplayOptions &baseOptionsIn)
{
baseImageOptions = baseOptionsIn;
if(baseImageOptions.onlyAspect == ImageAspectScreenFilter_Monitor)
{
imageAspectMatchesMonitor = true;
baseImageOptions.onlyAspect = width() >= height() ? ImageAspectScreenFilter_Landscape : ImageAspectScreenFilter_Portrait;
}
}
void MainWindow::setImageSwitcher(ImageSwitcher *switcherIn)
{
switcher = switcherIn;
}
const ImageDisplayOptions &MainWindow::getBaseOptions()
{
return baseImageOptions;
}

View File

@@ -3,6 +3,8 @@
#include <QMainWindow>
#include <QPixmap>
#include "imagestructs.h"
#include "imageselector.h"
namespace Ui {
class MainWindow;
@@ -10,6 +12,7 @@ class MainWindow;
class QLabel;
class QKeyEvent;
class Overlay;
class ImageSwitcher;
class MainWindow : public QMainWindow
{
@@ -21,29 +24,32 @@ public:
bool event(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
~MainWindow();
void setImage(std::string path);
void setImage(const ImageDetails &imageDetails);
void setBlurRadius(unsigned int blurRadius);
void setBackgroundOpacity(unsigned int opacity);
void setTransitionTime(unsigned int transitionSeconds);
void warn(std::string text);
void setOverlay(Overlay* overlay);
void setAspect(char aspectIn);
void setDebugMode(bool debugModeIn) {debugMode = debugModeIn;}
void setFitAspectAxisToWindow(bool fitAspectAxisToWindowIn) { fitAspectAxisToWindow = fitAspectAxisToWindowIn; }
void setOverlay(std::unique_ptr<Overlay> &overlay);
void setBaseOptions(const ImageDisplayOptions &baseOptionsIn);
const ImageDisplayOptions &getBaseOptions();
void setImageSwitcher(ImageSwitcher *switcherIn);
void setOverlayHexRGB(QString overlayHexRGB);
public slots:
void checkWindowSize();
private:
Ui::MainWindow *ui;
std::string currentImage;
unsigned int blurRadius = 20;
unsigned int backgroundOpacity = 150;
ImageDisplayOptions baseImageOptions;
bool imageAspectMatchesMonitor = false;
ImageDetails currentImage;
QSize lastScreenSize = {0,0};
QString overlayHexRGB = "#FFFF";
unsigned int transitionSeconds = 1;
char aspect = 'a';
bool debugMode = false;
bool fitAspectAxisToWindow = false;
QString overlayHexRGB;
Overlay* overlay = NULL;
std::unique_ptr<Overlay> overlay;
ImageSwitcher *switcher = nullptr;
void drawText(QPixmap& image, int margin, int fontsize, QString text, int alignment);

View File

@@ -1,4 +1,5 @@
#include "overlay.h"
#include "logger.h"
#include <QString>
#include <QDateTime>
#include <libexif/exif-data.h>
@@ -50,10 +51,7 @@ void Overlay::parseInput() {
QString Overlay::getTemplate(QStringList components){
if (components.size()>3) {
if(debugMode)
{
std::cout << "template: " << components[3].toStdString() << std::endl;
}
Log("template: ", components[3].toStdString());
return components[3];
}
return "";
@@ -61,10 +59,7 @@ QString Overlay::getTemplate(QStringList components){
int Overlay::getMargin(QStringList components){
if (components.size()>1) {
if(debugMode)
{
std::cout << "margin: " << components[1].toStdString() << std::endl;
}
Log("margin: ", components[1].toStdString());
int num = components[1].toInt();
if (num > 0) {
return num;
@@ -76,10 +71,7 @@ int Overlay::getMargin(QStringList components){
int Overlay::getFontsize(QStringList components){
if (components.size()>2) {
if(debugMode)
{
std::cout << "fontsize: " << components[2].toStdString() << std::endl;
}
Log("fontsize: ", components[2].toStdString());
int num = components[2].toInt();
if (num > 0) {
return num;

View File

@@ -27,13 +27,10 @@ class Overlay
int getMarginBottomRight();
int getFontsizeBottomRight();
void setDebugMode(const bool debugModeIn) { debugMode = debugModeIn; }
private:
const std::string overlayInput;
int margin;
int fontsize;
bool debugMode = false;
QString topLeftTemplate;
QString topRightTemplate;

View File

@@ -1,12 +1,15 @@
#include "pathtraverser.h"
#include "mainwindow.h"
#include "appconfig.h"
#include "logger.h"
#include <QDirIterator>
#include <QTimer>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <iostream>
#include <stdlib.h> /* srand, rand */
PathTraverser::PathTraverser(const std::string path):
path(path)
{}
@@ -20,6 +23,13 @@ QStringList PathTraverser::getImageFormats() const {
return imageFormats;
}
ImageDisplayOptions PathTraverser::LoadOptionsForDirectory(const std::string &directoryPath, const ImageDisplayOptions &baseOptions) const
{
Config baseConfig;
baseConfig.baseDisplayOptions = baseOptions;
return getConfigurationForFolder(directoryPath, baseConfig).baseDisplayOptions;
}
RecursivePathTraverser::RecursivePathTraverser(const std::string path):
PathTraverser(path)
{}
@@ -44,6 +54,12 @@ const std::string RecursivePathTraverser::getImagePath(const std::string image)
return image;
}
ImageDisplayOptions RecursivePathTraverser::UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const
{
QDir d = QFileInfo(filename.c_str()).absoluteDir();
return LoadOptionsForDirectory(d.absolutePath().toStdString(), baseOptions);
}
DefaultPathTraverser::DefaultPathTraverser(const std::string path):
PathTraverser(path),
directory(path.c_str())
@@ -61,3 +77,38 @@ const std::string DefaultPathTraverser::getImagePath(const std::string image) co
{
return directory.filePath(QString(image.c_str())).toStdString();
}
ImageDisplayOptions DefaultPathTraverser::UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const
{
Q_UNUSED(filename);
return LoadOptionsForDirectory(directory.absolutePath().toStdString(), baseOptions);
}
ImageListPathTraverser::ImageListPathTraverser(const std::string &imageListString):
PathTraverser("")
{
QString str = QString(imageListString.c_str());
imageList = str.split(QLatin1Char(','));
}
ImageListPathTraverser::~ImageListPathTraverser() {}
QStringList ImageListPathTraverser::getImages() const
{
return imageList;
}
const std::string ImageListPathTraverser::getImagePath(const std::string image) const
{
return image;
}
ImageDisplayOptions ImageListPathTraverser::UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const
{
// no per file options modification supported
Q_UNUSED(filename);
Q_UNUSED(baseOptions);
return baseOptions;
}

View File

@@ -4,6 +4,7 @@
#include <iostream>
#include <QDir>
#include <QStringList>
#include "imageselector.h"
static const QStringList supportedFormats={"jpg","jpeg","png","tif","tiff"};
@@ -15,10 +16,12 @@ class PathTraverser
virtual ~PathTraverser();
virtual QStringList getImages() const = 0;
virtual const std::string getImagePath(const std::string image) const = 0;
virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const = 0;
protected:
const std::string path;
QStringList getImageFormats() const;
ImageDisplayOptions LoadOptionsForDirectory(const std::string &directoryPath, const ImageDisplayOptions &baseOptions) const;
};
class RecursivePathTraverser : public PathTraverser
@@ -28,6 +31,7 @@ class RecursivePathTraverser : public PathTraverser
virtual ~RecursivePathTraverser();
QStringList getImages() const;
virtual const std::string getImagePath(const std::string image) const;
virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const;
};
class DefaultPathTraverser : public PathTraverser
@@ -37,8 +41,20 @@ class DefaultPathTraverser : public PathTraverser
virtual ~DefaultPathTraverser();
QStringList getImages() const;
virtual const std::string getImagePath(const std::string image) const;
virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const;
private:
QDir directory;
};
class ImageListPathTraverser : public PathTraverser
{
public:
ImageListPathTraverser(const std::string &imageListString);
virtual ~ImageListPathTraverser();
QStringList getImages() const;
virtual const std::string getImagePath(const std::string image) const;
virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& options) const;
private:
QStringList imageList;
};
#endif // PATHTRAVERSER_H

View File

@@ -5,6 +5,9 @@
#-------------------------------------------------
QT += core gui
CONFIG += qt
CONFIG += debug
CONFIG += c++1z
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
@@ -33,14 +36,20 @@ SOURCES += \
imageswitcher.cpp \
pathtraverser.cpp \
overlay.cpp \
imageselector.cpp
imageselector.cpp \
appconfig.cpp \
imagestructs.cpp \
logger.cpp
HEADERS += \
mainwindow.h \
imageselector.h \
pathtraverser.h \
overlay.h \
imageswitcher.h
imageswitcher.h \
imagestructs.h \
appconfig.h \
logger.h
FORMS += \
mainwindow.ui