Skip to main content

Tutorials

Posts that are tutorials

The cover of "Writing statistics for your Eleventy (11ty) blog"

Writing statistics for your Eleventy (11ty) blog

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

One of the reasons why I switched to Eleventy was to make it easier to do custom stuff, like creating a writing statistics page for my needs. This blog post is to share how I created that page and how you can create one for your blog.

Read more of "Writing statistics for your Eleventy (11ty) blog"

The cover of "Detox and React Native: UI testing Android permission flows"

Detox and React Native: UI testing Android permission flows

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I’m currently learning React Native and Expo as an alternative to native app development. To do this, I’ve been working on an app so that I can get hands-on experience with React Native app development. This includes end-to-end UI testing, using Detox.

I was surprised that Detox doesn’t have an API to interact with permissions. The device.launchApp function does have a permissions field, but this only works on iOS and runs on app start-up - it doesn’t allow you to test the actual user flow.

In this article, I will explain how you can do end-to-end testing with Android permissions, including simulating user interaction with the Android permission request modal.

Read more of "Detox and React Native: UI testing Android permission flows"

Better word counts and reading time in Eleventy (11ty)

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I recently switched to using Eleventy to generate my blog. As part of this process, I needed to implement word counts for each of my posts. I made my own plugin for this as I was unable to find an existing one that met my requirements.

Read more of "Better word counts and reading time in Eleventy (11ty)"

How to find backlinks using Eleventy (11ty)

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I recently switched to using Eleventy to generate my blog. Because Eleventy is very easy to extend, I was able to add more features to my blog - including backlinks.

Read more of "How to find backlinks using Eleventy (11ty)"

The cover of "SDL_GameController: Making gamepads just work"

SDL_GameController: Making gamepads just work

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

When implementing controller support in a game, it’s desirable for gamepads to just work without a lot of user configuration. Platform APIs are pretty useless for this, the solution is an API like SDL_GameController that allows you to target a large number of gamepads without much effort.

Each operating system has its own API for gamepad input. Windows has XInput, and Linux has the joystick and evdev APIs. When a gamepad button is pressed, applications will receive a button id. This is a number, there’s no OS way to know which button id corresponds with which button. The ids for a button are not the same on different gamepads and platforms, making it super hard to support more than a couple of devices.

if (SDL_JoystickGetButton(joystick, 8)) {
    std::cerr << "no idea what button 8 is" << std::endl;
}

One thing platforms do give you is the name, model, and manufacturer of the game controller. If you test with a large number of gamepads, you can create a database from gamepad name to layout. Luckily, SDL_GameController has already done this for you. Instead of a random number, you can use a named button that will work no matter the gamepad and platform:

if (SDL_GameControllerGetButton(controller, SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X)) {
    std::cerr << "X was pressed!" << std::endl;
}

Read more of "SDL_GameController: Making gamepads just work"

The cover of "Luanti (Minetest) on Steam Deck: usage and review"

Luanti (Minetest) on Steam Deck: usage and review

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

A few months ago, my Steam Deck arrived. So, obviously, one of the first things I did was play Luanti. Whilst Luanti isn’t available on Steam yet, it is available on Flatpak and so can be installed from desktop mode.

Luanti plays very well on the Steam Deck, it performs great and Steam Input makes setting up controls easy. The Deck is an open console, so there’s full support for modding and you can cross-play with other platforms.

In this article, I will explain how to set up Luanti on the deck, and review the controls, performance, and experience. I’ve already written my thoughts about the Deck in another article, and recommend checking that out.

Read more of "Luanti (Minetest) on Steam Deck: usage and review"

The cover of "Setting up fingerprint auth on Kubuntu (Thinkpad X1)"

Setting up fingerprint auth on Kubuntu (Thinkpad X1)

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Using a fingerprint to unlock your computer or elevate privileges (sudo) is a nice convenience, saving time during daily use of a computer. This article will show you how to set up fingerprint authentication on Ubuntu KDE. You’ll still need to enter your password to login, but once logged in you will be able to use a fingerprint to unlock or use sudo.

This guide is based on a Thinkpad X1 running Kubuntu 22.04, but it should work for most laptops with supported fingerprint sensors. You can check whether your fingerprint sensor is supported by searching for “Linux fingerprint” and your computer’s make/model. The Arch wiki is especially useful for this sort of thing, even if you don’t use Arch.

It goes without saying that this comes without warranty; backup your data, and have a LiveUSB ready. There are some pointers on recovering from mistakes at the bottom of this page. Luckily, I never needed to use a LiveUSB, I could easily recover from a virtual console.

Read more of "Setting up fingerprint auth on Kubuntu (Thinkpad X1)"

The cover of "Devkit CLI: Upload your game to Steam Deck from your dev computer"

Devkit CLI: Upload your game to Steam Deck from your dev computer

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Steam Deck is a new portable gaming console from Valve. The Deck is a very open platform as it’s a full Linux PC, making it very easy to use third-party games and stores. All Decks can act as a devkit machine, and come with devkit tools preinstalled. There was a Steam Deck Devkit edition, but that was a preproduction version of the hardware to allow game developers to test their games.

This article will show you how to upload and run your games on the Steam Deck from your development computer, using a VSCode task or a script you can use in any editor. Whilst you could develop on the Deck directly, it would require setting up a new dev environment. Being able to develop on your computer and test on the Deck allows you to reuse your current dev environment.

Read more of "Devkit CLI: Upload your game to Steam Deck from your dev computer"

The cover of "Creating worker NPCs using behavior trees"

Creating worker NPCs using behavior trees

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I’m a huge fan of RimWorld, a base building game where you manage a group of colonists. Rather than directly controlling the colonists, you place blueprints for buildings, mark trees for cutting, and animals for hunting. NPCs will then decide what to do automatically, based on their skills and priorities.

I’ve made two games recently with similar mechanics. The first was Ruben’s Virtual World Project (RVWP), a hybrid basebuilder/topdown shooter. The second was Tin Mining, a mining sim created as an entry to Ludum Dare 48. Both of these games allowed placing building plans that NPC workers would then build out.

In this article, I will explain how I implemented the NPC AIs, and the problems I faced.

Read more of "Creating worker NPCs using behavior trees"

The cover of "Securing Markdown user content with Mozilla Bleach"

Securing Markdown user content with Mozilla Bleach

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Markdown is a common choice for rich text formatting due to its readability and ease-of-use. Unlike a lot of markup, it aims to match natural text. It’s even easy for beginner users, and there are WYSIWYG editors available.

We will be using the Python Markdown library to convert Markdown to HTML. Markdown doesn’t have a well-defined standard. The library aims to comply with what little is defined by the Markdown syntax specification, meaning that it is also often stricter than other parsers.

Read more of "Securing Markdown user content with Mozilla Bleach"

The cover of "Writing a Lua sandbox using sol2 / sol3"

Writing a Lua sandbox using sol2 / sol3

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Sandboxing can protect the user’s computer from malicious or buggy scripts. But sandboxes are difficult to get right; you need to be very careful with what you expose, and make sure you test for vulnerabilities. The Sandboxes on the Lua wiki is required reading, as it contains very helpful advice.

Read more of "Writing a Lua sandbox using sol2 / sol3"

The cover of "Extending sol3's implicit type conversion"

Extending sol3's implicit type conversion

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Many APIs in my game push Vector3s to and from Lua. It’s such a common operation, that most of my functions used to look like this:

sol::table add(sol::table tPos) {
    Vector3f pos = TableToPos(tPos);

    // do something
    return PosToTable(pos);
}

One of the benefits of sol is that it is able to bind Lua arguments to C++ function arguments, converting types implicitly. Having to convert from a table to a vector ourselves is quite annoying. It would be much nicer to have sol do it for us. Luckily, sol allows you to customise how types are retrieved and pushed to Lua using Customisation Points.

When trying to convert a type from Lua to C++, sol will call certain templated functions. We will be customisating sol’s behaviour using a technique called template specialization, which allows us to specialise a specific instance of the templated functions and structs. By the end of this article, we’ll be able to use Vector3 directly when using sol, allowing the above code to be turned into this:

Vector3f add(Vector3f pos) {
    // do something

    return pos;
}

Read more of "Extending sol3's implicit type conversion"

Android: Complete, generic data-binding RecyclerView adapter

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Data binding greatly reduces the amount of code you need to connect user-interfaces with ViewModels. It keeps Activity and Fragment code small, and makes it easier to manage lifecycles.

<EditText
    android:id="@+id/username"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="@={viewModel.username}"/>

I discovered that there was no attribute to bind the elements in a RecyclerView, due to the fact that a RecyclerView needs an adapter to be able to create element views. It would also be nice to automatically use data binding to create the viewholders. There are a number of guides to do both of these halves, but I now present the code to do the whole.

Read more of "Android: Complete, generic data-binding RecyclerView adapter"

The cover of "A Comparison of SFML GUI Libraries: TGUI vs SFGUI vs ImGui"

A Comparison of SFML GUI Libraries: TGUI vs SFGUI vs ImGui

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

SFML is an excellent library that can be used to create 2D games and similar applications in C++. It’s an abstraction over OpenGL and various system APIs, presenting a consistent and easy-to-use interface.

Providing a Graphical User Interface (GUI / UI) API is out of scope for SFML. GUIs are complicated, and there’s no single good way to implement them. The S in SFML stands for Simple but GUI code rarely is.

There are many different options to choose from when making GUIs. This article is an in-depth comparison of the options for making GUIs in SFML, discussing their pros and cons.

Read more of "A Comparison of SFML GUI Libraries: TGUI vs SFGUI vs ImGui"

The cover of "Rendering a topdown world with layers and z-levels using SFML"

Rendering a topdown world with layers and z-levels using SFML

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Ruben’s Virtual World Project is a game I’ve been working on for almost 4 years now. Recently I rewrote the rendering code to support voxel lighting and multiple z-level - heights of the map.

Read more of "Rendering a topdown world with layers and z-levels using SFML"

Getting rid of YouTube Music's "Are you still listening?" dialog

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

YouTube Music is a great way to listen for music for free, and with no adverts if you use an adblocker. There is one annoying problem however: after listening for a while, Youtube will keep pausing the music to show a dialog which says “Are you still listening?”. This article will show how to automatically confirm the dialog.

Read more of "Getting rid of YouTube Music's "Are you still listening?" dialog"

C++: Self-registering functions using macros for test libraries

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Google’s C++ testing library has a nice syntax for registering tests, without needing to remember to add the tests to some central index. This article will show how to use macros to allow the creation of tests using only the following code:

Test(IntegerComparisonTest) {
    int a = 3;
    assert(a == 3);
}

Read more of "C++: Self-registering functions using macros for test libraries"

Porting C++ programs from Linux to Windows using vcpkg, CMake, and Visual Studio

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Recently I had to reinstall Windows to debug a hardware issue. I decided to try to make the most of this by trying to build my game on Windows.

Read more of "Porting C++ programs from Linux to Windows using vcpkg, CMake, and Visual Studio"

RimWorld: Create timelapse from Progress Renderer on Linux

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Progress Renderer (Steam | GitHub) is a brilliant mod that takes a screenshot once a day, and dumps it into a folder.

The following bash script can be used to generate an mp4 timelapse of the screenshots. Simply specify a region to cut out of the screenshots, and some other parameters, and run the script in the working directory.

Read more of "RimWorld: Create timelapse from Progress Renderer on Linux"

Finding and copying DLLs from MinGW-W64 directories to bin directory

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

After cross-compiling your project for Windows, you find that it crashes due to missing DLLs. I will show how to identify any required DLLs using objdump, and copy them to your build directory.

Read more of "Finding and copying DLLs from MinGW-W64 directories to bin directory"

The cover of "Install Autodesk Maya on Ubuntu 17.04"

Install Autodesk Maya on Ubuntu 17.04

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Whilst Maya claims to have Linux support, it only officially supports RedHat and CentOS Linux. It is, however, still possible to install it on Debian and Ubuntu based systems with some leg work.

Read more of "Install Autodesk Maya on Ubuntu 17.04"

Mingw-w64 and CMake: unrecognised option -rdynamic on Ubuntu

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I had an issue where CMake was failing on a compiler test with the following error:

error: unrecognized option '-rdynamic'

The problem was that CMake caches settings such as compiler flags in CMakeCache.txt, so you need to clear the cache when changing the platform. Do this by deleting CMakeFiles and CMakeCache.txt

Read more of "Mingw-w64 and CMake: unrecognised option -rdynamic on Ubuntu"

The cover of "OpenCV: compile images (Mats) into a strip"

OpenCV: compile images (Mats) into a strip

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I created a very short C++ snippet to accumulate a series of Mats into a single Mat strip. It works like acc = acc + m - a new mat is added to the accumulator each time, then stored in the accumulator again.

Read more of "OpenCV: compile images (Mats) into a strip"

How to emit a single particle using SFML's Thor Particle System

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Usecase: shells dropping in sync with firing, fake bullets, etc

You must use a particle emitter to create particles, however this doesn’t mean it’s impossible to create single particles on command. You can create a particle emitter which simply adds particles from a queue to the system

Read more of "How to emit a single particle using SFML's Thor Particle System"

How to center a SFGUI window (sfg::Window)

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Simply get the SFGUI window size using GetAllocation, the sfml window size using getSize, then do this arithmetic:

auto window = sfg::Window::Create();
auto win_rect = window->GetAllocation();
sf::Vector2f size(win_rect.width, win_rect.height);
window->SetPosition(((sf::Vector2f)rwindow->getSize() - size) / 2.0f);

Read more of "How to center a SFGUI window (sfg::Window)"

Setting up a MonoDevelop solution and project for RimWorld modding

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

This tutorial will show you how to create a new MonoDevelop solution and project, and how to properly connect it to RimWorld. You’ll need to have an installation of Mono that supports .NET 3.5 - here is a tutorial on how to install it.

Read more of "Setting up a MonoDevelop solution and project for RimWorld modding"

Install Monodevelop to build for .NET 3.5 (using Mono 3.14) - RimWorld

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

This tutorial will show you how to install Mono and Monodevelop in order to develop .NET 3.5 projects. This is useful when writing C# assembly mods for Unity engine based games, such as RimWorld, as they tend to require .NET 3.5.

Read more of "Install Monodevelop to build for .NET 3.5 (using Mono 3.14) - RimWorld"

Move private repos from Bitbucket to GitHub

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Now that Github supports unlimited private repos in all plans, you might as well keep things all together (although definitely have backups elsewhere incase Github is DDOS’d again, dies or goes evil). Simply change “rubenwardy” to your username and “XXXX” to a personal access token with “repo” checked. Also make sure you have an SSH key for Github.

Read more of "Move private repos from Bitbucket to GitHub"

Xubuntu / Xfce sync whisker menu favourites

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I use a dual monitor setup. For each monitor I have a panel, and each panel has a whisker menu. I found that the favourites section of each of these panels is not synchronised. To fix this, I wrote a simple script.

Read more of "Xubuntu / Xfce sync whisker menu favourites"

Dual boot Ubuntu on ASUS X555LA UEFI laptop alongside Windows 10

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I recently successfully dual booted Ubuntu on an ASUS X555LA laptop, the X555LAB variety. This guide will work on most Ubuntu derivatives, I installed Xubuntu using these steps. The only things that are different between them are window managers and preinstalled software.

Read more of "Dual boot Ubuntu on ASUS X555LA UEFI laptop alongside Windows 10"

Linking a user account to an external phpBB forum

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

This article will show you how to verify a user’s identity by letting them associate their account with an external third party phpBB account. I used Python and Flask to achieve this, however any language and framework should work, and shouldn’t be too hard to port to.

Read more of "Linking a user account to an external phpBB forum"

Getting user profile data from external phpBB forums

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

I recently wrote and released a python module to allow fetching of profile data.

You can install it using pip:

pip install beautifulsoup4 phpbb-parser

Here’s how you import and use a profile:

import phpbb_parser as parser

username = "rubenwardy"
profile = parser.get_profile("https://forum.minetest.net", username)

if profile:
    signature = profile.signature.text
    location = profile.get("location") or "unknown"
    github = profile.get("github") or "none"

    print(username + " from " + location + " has github " + github)
    print("Signatue: " + signature.text)
else:
    print("Could not get profile!")

profile.signature is a beautifulsoup4 object.

Next: Linking a user account to an external phpBB forum.

Read more of "Getting user profile data from external phpBB forums"

Convert folder of images to PDF with filenames

Andrew Ward

rubenwardy

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Here is a shell script specific for GNU/Linux based operating systems to use. On different operating systems the convert commands will be the same, but the for loop will be different due to a different batch file syntax.

rm /tmp/imageex -r
mkdir /tmp/imageex
for filename in *.png; do
    echo "Processing $filename"
    convert $filename -background White \
        label:$filename -gravity Center \
         -append -pointsize 14 \
        /tmp/imageex/$filename.png
done
echo "Exporting to PDF..."
convert /tmp/imageex/*.png output.pdf
echo "Done."

Read more of "Convert folder of images to PDF with filenames"