diff --git a/.gitignore b/.gitignore index 258bec7..d46ff1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ make .swp .git +build +.vscode diff --git a/Makefile b/Makefile index d7c1d8e..462e726 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ check-deps-deb: clean: rm -rf build -build: +build: $(shell find src -type f) mkdir -p build qmake src/slide.pro -o build/Makefile make -C build diff --git a/README.md b/README.md index f2880ea..0246fe4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This project is maintained by myself during my spare time. If you like and use i ## Usage ``` -slide [-t rotation_seconds] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-O overlay_string] +slide [-t rotation_seconds] [-a aspect] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-O overlay_string] [-v] [--verbose] [--stretch] ``` * `image_folder`: where to search for images (.jpg files) @@ -25,8 +25,11 @@ slide [-t rotation_seconds] [-o background_opacity(0..255)] [-b blur_radius] -p * `-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) 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. * `-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 `;` diff --git a/src/imageselector.cpp b/src/imageselector.cpp index 3d082d6..5271291 100644 --- a/src/imageselector.cpp +++ b/src/imageselector.cpp @@ -5,21 +5,86 @@ #include #include #include +#include #include #include /* srand, rand */ #include /* time */ #include // std::shuffle #include // std::default_random_engine -ImageSelector::ImageSelector(std::unique_ptr& pathTraverser): - pathTraverser(pathTraverser) +ImageSelector::ImageSelector(std::unique_ptr& pathTraverser, char aspectIn): + pathTraverser(pathTraverser), aspect(aspectIn) { } ImageSelector::~ImageSelector(){} -RandomImageSelector::RandomImageSelector(std::unique_ptr& pathTraverser): - ImageSelector(pathTraverser) +int ImageSelector::getImageRotation(const std::string &fileName) +{ + int orientation = 0; + ExifData *exifData = exif_data_new_from_file(fileName.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; +} + +bool ImageSelector::imageValidForAspect(const std::string &fileName) +{ + QPixmap p( fileName.c_str() ); + int imageWidth = p.width(); + int imageHeight = p.height(); + int rotation = getImageRotation(fileName); + if ( rotation == 90 || rotation == 270 ) + { + std::swap(imageWidth,imageHeight); + } + + switch(aspect) + { + case 'a': + // allow all + break; + case 'l': + if ( imageWidth < imageHeight ) + { + return false; + } + break; + case 'p': + if ( imageHeight < imageWidth ) + { + return false; + } + break; + } + return true; +} + + +RandomImageSelector::RandomImageSelector(std::unique_ptr& pathTraverser, char aspect): + ImageSelector(pathTraverser, aspect) { srand (time(NULL)); } @@ -31,9 +96,17 @@ std::string RandomImageSelector::getNextImage() std:: string filename; try { - QStringList images = pathTraverser->getImages(); - unsigned int selectedImage = selectRandom(images); - filename = pathTraverser->getImagePath(images.at(selectedImage).toStdString()); + while (filename.empty()) + { + QStringList images = pathTraverser->getImages(); + unsigned int selectedImage = selectRandom(images); + filename = pathTraverser->getImagePath(images.at(selectedImage).toStdString()); + if (!imageValidForAspect(filename)) + { + filename.clear(); + } + } + } catch(const std::string& err) { @@ -45,7 +118,10 @@ std::string RandomImageSelector::getNextImage() unsigned int RandomImageSelector::selectRandom(const QStringList& images) const { - std::cout << "images: " << images.size() << std::endl; + if(debugMode) + { + std::cout << "images: " << images.size() << std::endl; + } if (images.size() == 0) { throw std::string("No jpg images found in given folder"); @@ -53,8 +129,8 @@ unsigned int RandomImageSelector::selectRandom(const QStringList& images) const return rand() % images.size(); } -ShuffleImageSelector::ShuffleImageSelector(std::unique_ptr& pathTraverser): - ImageSelector(pathTraverser), +ShuffleImageSelector::ShuffleImageSelector(std::unique_ptr& pathTraverser, char aspect): + ImageSelector(pathTraverser, aspect), current_image_shuffle(-1), images() { @@ -83,8 +159,20 @@ std::string ShuffleImageSelector::getNextImage() std::string filename = pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()); if(!QFileInfo::exists(QString(filename.c_str()))) { - std::cout << "file not found: " << filename << std::endl; - current_image_shuffle = images.size(); + if(debugMode) + { + std::cout << "file not found: " << filename << std::endl; + } + current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image + return getNextImage(); + } + if (!imageValidForAspect(filename)) + { + if(debugMode) + { + std::cout << "image has invalid aspect: " << filename << "(images left:" << (images.size()-current_image_shuffle) << ")" << std::endl; + } + current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image return getNextImage(); } std::cout << "updating image: " << filename << std::endl; @@ -92,8 +180,8 @@ std::string ShuffleImageSelector::getNextImage() return filename; } -SortedImageSelector::SortedImageSelector(std::unique_ptr& pathTraverser): - ImageSelector(pathTraverser), +SortedImageSelector::SortedImageSelector(std::unique_ptr& pathTraverser, char aspect): + ImageSelector(pathTraverser, aspect), images() { srand (time(NULL)); @@ -121,10 +209,13 @@ std::string SortedImageSelector::getNextImage() { images = pathTraverser->getImages(); std::sort(images.begin(), images.end()); - std::cout << "read " << images.size() << " images." << std::endl; - for (int i = 0;i getImagePath(images.takeFirst().toStdString()); if(!QFileInfo::exists(QString(filename.c_str()))) { - std::cout << "file not found: " << filename << std::endl; + if(debugMode) + { + std::cout << "file not found: " << filename << std::endl; + } return getNextImage(); } + if (!imageValidForAspect(filename)) + { + if(debugMode) + { + std::cout << "image has invalid aspect: " << filename << std::endl; + } + return getNextImage(); + } + std::cout << "updating image: " << filename << std::endl; return filename; } diff --git a/src/imageselector.h b/src/imageselector.h index dc73b83..645f5af 100644 --- a/src/imageselector.h +++ b/src/imageselector.h @@ -11,18 +11,23 @@ class PathTraverser; class ImageSelector { public: - ImageSelector(std::unique_ptr& pathTraverser); + ImageSelector(std::unique_ptr& pathTraverser, char aspectIn); virtual ~ImageSelector(); virtual std::string getNextImage() = 0; + void setDebugMode(bool debugModeIn) { debugMode = debugModeIn;} protected: + int getImageRotation(const std::string &fileName); + bool imageValidForAspect(const std::string &fileName); std::unique_ptr& pathTraverser; + char aspect; + bool debugMode = false; }; class RandomImageSelector : public ImageSelector { public: - RandomImageSelector(std::unique_ptr& pathTraverser); + RandomImageSelector(std::unique_ptr& pathTraverser, char aspect); virtual ~RandomImageSelector(); virtual std::string getNextImage(); @@ -33,7 +38,7 @@ private: class ShuffleImageSelector : public ImageSelector { public: - ShuffleImageSelector(std::unique_ptr& pathTraverser); + ShuffleImageSelector(std::unique_ptr& pathTraverser, char aspect); virtual ~ShuffleImageSelector(); virtual std::string getNextImage(); @@ -45,7 +50,7 @@ private: class SortedImageSelector : public ImageSelector { public: - SortedImageSelector(std::unique_ptr& pathTraverser); + SortedImageSelector(std::unique_ptr& pathTraverser, char aspect); virtual ~SortedImageSelector(); virtual std::string getNextImage(); diff --git a/src/imageswitcher.cpp b/src/imageswitcher.cpp index e64979d..5e57331 100644 --- a/src/imageswitcher.cpp +++ b/src/imageswitcher.cpp @@ -14,7 +14,8 @@ ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeout, std::unique_pt window(w), timeout(timeout), selector(selector), - timer(this) + timer(this), + timerNoContent(this) { } @@ -24,10 +25,12 @@ void ImageSwitcher::updateImage() if (filename == "") { window.warn("No image found."); + timerNoContent.start(timeoutNoContent); } else { window.setImage(filename); + timerNoContent.stop(); // we have loaded content so stop the fast polling } } @@ -35,5 +38,6 @@ void ImageSwitcher::start() { updateImage(); connect(&timer, SIGNAL(timeout()), this, SLOT(updateImage())); + connect(&timerNoContent, SIGNAL(timeout()), this, SLOT(updateImage())); timer.start(timeout); } diff --git a/src/imageswitcher.h b/src/imageswitcher.h index d49d3ad..f0f665c 100644 --- a/src/imageswitcher.h +++ b/src/imageswitcher.h @@ -22,6 +22,8 @@ private: unsigned int timeout; std::unique_ptr& selector; QTimer timer; + const unsigned int timeoutNoContent = 5 * 1000; // 5 sec + QTimer timerNoContent; }; #endif // IMAGESWITCHER_H diff --git a/src/main.cpp b/src/main.cpp index 8943b49..accc571 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,13 +8,14 @@ #include #include +#include #include #include #include #include void usage(std::string programName) { - std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-s]" << std::endl; + std::cerr << "Usage: " << programName << " [-t rotation_seconds] [-a aspect('l','p','a')] [-o background_opacity(0..255)] [-b blur_radius] -p image_folder [-r] [-s] [-v] [--verbose] [--stretch]" << std::endl; } int main(int argc, char *argv[]) @@ -29,12 +30,39 @@ int main(int argc, char *argv[]) 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 = ""; - while ((opt = getopt(argc, argv, "b:p:t:o:O:rsS")) != -1) { + int debugInt = 0; + int stretchInt = 0; + static struct option long_options[] = + { + {"verbose", no_argument, &debugInt, 1}, + {"stretch", no_argument, &stretchInt, 1}, + }; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "b:p:t:o:O:a: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; + break; case 'p': path = optarg; break; + case 'a': + aspect = optarg[0]; + if ( valid_aspects.find(aspect) == std::string::npos ) + { + std::cout << "Invalid Aspect option, defaulting to all" << std::endl; + aspect = 'a'; + } + break; case 't': rotationSeconds = atoi(optarg); break; @@ -57,11 +85,22 @@ int main(int argc, char *argv[]) case 'O': overlay = optarg; break; + case 'v': + debugMode = true; + break; default: /* '?' */ usage(argv[0]); return 1; } } + if(debugInt==1) + { + debugMode = true; + } + if(stretchInt==1) + { + fitAspectAxisToWindow = true; + } if (path.empty()) { @@ -83,19 +122,28 @@ int main(int argc, char *argv[]) std::unique_ptr selector; if (sorted) { - selector = std::unique_ptr(new SortedImageSelector(pathTraverser)); + selector = std::unique_ptr(new SortedImageSelector(pathTraverser, aspect)); } else if (shuffle) { - selector = std::unique_ptr(new ShuffleImageSelector(pathTraverser)); + selector = std::unique_ptr(new ShuffleImageSelector(pathTraverser, aspect)); } else { - selector = std::unique_ptr(new RandomImageSelector(pathTraverser)); + selector = std::unique_ptr(new RandomImageSelector(pathTraverser, aspect)); + } + selector->setDebugMode(debugMode); + if(debugMode) + { + std::cout << "Rotation Time: " << rotationSeconds << std::endl; + std::cout << "Overlay input: " << overlay << std::endl; } - std::cout << "Overlay input: " << overlay << std::endl; Overlay o(overlay); + o.setDebugMode(debugMode); w.setOverlay(&o); + w.setAspect(aspect); + w.setDebugMode(debugMode); + w.setFitAspectAxisToWindow(fitAspectAxisToWindow); w.show(); ImageSwitcher switcher(w, rotationSeconds * 1000, selector); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9576a80..6c0aa85 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -109,6 +109,11 @@ void MainWindow::updateImage(bool immediately) } QPixmap p( currentImage.c_str() ); + if(debugMode) + { + std::cout << "size:" << p.width() << "x" << p.height() << std::endl; + } + QPixmap rotated = getRotatedPixmap(p); QPixmap scaled = getScaledPixmap(rotated); QPixmap background = getBlurredBackground(rotated, scaled); @@ -120,6 +125,21 @@ void MainWindow::updateImage(bool immediately) 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) + { + // draw a thumbnail version of the source image in the bottom left, to check for cropping issues + QPainter pt(&background); + QBrush brush(QColor(255, 255, 255, 255)); + int margin = 10; + QPixmap thumbNail = p.scaledToWidth(200, Qt::SmoothTransformation); + pt.fillRect(background.width() - thumbNail.width() - 2*margin, + background.height()-thumbNail.height() - 2*margin, + thumbNail.width() +2*margin, thumbNail.height()+2*margin, brush); + + pt.drawPixmap( background.width() - thumbNail.width() - margin, + background.height()-thumbNail.height() - margin, + thumbNail); + } } label->setPixmap(background); @@ -164,13 +184,22 @@ void MainWindow::setOverlay(Overlay* o) overlay = o; } +void MainWindow::setAspect(char aspectIn) +{ + aspect = aspectIn; +} + QPixmap MainWindow::getBlurredBackground(const QPixmap& originalSize, const QPixmap& scaled) { - if (scaled.width() < width()) { + if (fitAspectAxisToWindow) { + // our scaled version will just fill the whole screen, us it directly + return scaled.copy(); + } else if (scaled.width() < width()) { QPixmap background = blur(originalSize.scaledToWidth(width(), Qt::SmoothTransformation)); QRect rect(0, (background.height() - height())/2, width(), height()); return background.copy(rect); } else { + // aspect 'p' or the image is not as wide as the screen QPixmap background = blur(originalSize.scaledToHeight(height(), Qt::SmoothTransformation)); QRect rect((background.width() - width())/2, 0, width(), height()); return background.copy(rect); @@ -186,9 +215,28 @@ QPixmap MainWindow::getRotatedPixmap(const QPixmap& p) QPixmap MainWindow::getScaledPixmap(const QPixmap& p) { - int w = width(); - int h = height(); - return p.scaled(w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (fitAspectAxisToWindow) + { + if ( aspect == 'p') + { + // 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') + { + // 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()); + } + } + + // just scale the best we can for the given photo + int w = width(); + int h = height(); + return p.scaled(w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation); } void MainWindow::drawBackground(const QPixmap& originalSize, const QPixmap& scaled) diff --git a/src/mainwindow.h b/src/mainwindow.h index 362a0cc..d83f436 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -25,13 +25,18 @@ public: void setBackgroundOpacity(unsigned int opacity); 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; } private: Ui::MainWindow *ui; std::string currentImage; unsigned int blurRadius = 20; unsigned int backgroundOpacity = 150; + char aspect = 'a'; + bool debugMode = false; + bool fitAspectAxisToWindow = false; Overlay* overlay; diff --git a/src/overlay.cpp b/src/overlay.cpp index 9bd9aae..9d43572 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -50,7 +50,10 @@ void Overlay::parseInput() { QString Overlay::getTemplate(QStringList components){ if (components.size()>3) { - std::cout << "template: " << components[3].toStdString() << std::endl; + if(debugMode) + { + std::cout << "template: " << components[3].toStdString() << std::endl; + } return components[3]; } return ""; @@ -58,7 +61,10 @@ QString Overlay::getTemplate(QStringList components){ int Overlay::getMargin(QStringList components){ if (components.size()>1) { - std::cout << "margin: " << components[1].toStdString() << std::endl; + if(debugMode) + { + std::cout << "margin: " << components[1].toStdString() << std::endl; + } int num = components[1].toInt(); if (num > 0) { return num; @@ -70,7 +76,10 @@ int Overlay::getMargin(QStringList components){ int Overlay::getFontsize(QStringList components){ if (components.size()>2) { - std::cout << "fontsize: " << components[2].toStdString() << std::endl; + if(debugMode) + { + std::cout << "fontsize: " << components[2].toStdString() << std::endl; + } int num = components[2].toInt(); if (num > 0) { return num; diff --git a/src/overlay.h b/src/overlay.h index 3707c47..b1cef0a 100644 --- a/src/overlay.h +++ b/src/overlay.h @@ -27,11 +27,13 @@ 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;