A Case study - printing from DOS to Samba with CUPS as a back-end

Note that the following text may contain personal impressions, errors, blind areas and other flaws - it is by no means a replacement for official documentation. Feedback is welcome.

Contents

Why oh why

This "research" / tinkering spree on my part has been prompted by a friend, who took it into his head, that he'd like to print from his MS-DOS apps, running in native DOS on bare metal, to his relatively modern-day "driverless" inkjet, accessible via just USB and WiFi. The USB interface is "GDI class" (read: planned obsolescence after 2 years) and the LAN interface, although a little more open, requires a modern Apple-Flavoured protocol stack on the immediate client to operate.

As a sub-discipline of Linux printing, there's a traditional sport whose aim is to strap on PostScript printing capability onto your dirt-cheap scrap inkjet. And, there's a software package called CUPS, renowned for promoting this to a central principle of its operation :-) And, oh, it can do another nice trick or too, in addition.

In DOS, the printer device interfaces can be redirected. The options are limited, but the Novell and Microsoft networking stacks did contain support for that. Novell IPX/SPX is sadly and safely dead, but the Microsoft SMB/CIFS is alive and kicking, and available in Linux, courtesy of the Samba package/service.

My friend happens to have some Ubuntu machines about his place, and was not opposed to having some fun trying to splice it all together.

Being an occasional DOS mossback poseur, and a happy integrator-tinkerer, I accepted the challenge. To me, no whim is outrageous enough ;-)

DOS networking

My personal memory of the first half of the nineties is likely biased, subjective. At that time, a network of PC's would have a dedicated Novell Netware server. In fact, only later I learned that Microsoft had a network client for DOS too, that could talk to Windows servers - and maybe more. I only heard about Microsoft networking starting from approx. Windows 3.11 for Workgroups - and originally the network protocol was NetBEUI, rather than TCP/IP (which nowadays is the natural choice).

Novell Netware and Microsoft LAN networking are two competing proprietary protocol stacks for Ethernet (and other networks) to cater for "file sharing" (network file systems) and networked printing.

And then I noticed that there are networking utilities for DOS, typically implementations of some Internet applications / protocols (IETF standard), that did not rely on either Novell nor Microsoft protocol stacks. These "independent" apps typically needed a "packet driver" - synonymous with a so called CRYNWR API. Nowadays, let me mention the mTCP library and apps collection, started and maintained by Michael Brutman - requiring a CRYNWR packet driver.

Historically, there were dedicated HW-specific packet drivers for popular Ethernet hardware (NICs). Nowadays it's often easier to use a "conversion driver", a.k.a. a "shim", that provides the CRYNWR interface on top of Novell ODI or Microsoft NDIS API's. I.e., combine an odipkt or dis_pkt shim with a HW-specific driver for the respective popular proprietary stack.

For the "case at hand", where the objective was, to print from DOS to Linux-based Samba (etc), I have rehased the NetBootDisk 6.5, which apparently contains the Microsoft Network Client 3.0 for MS-DOS. I had NetBootDisk generate the config files for me, which I then copied, gutted some scripts, removed lots of the NBD dynamism by way of dead reckoning... to minimize the thing. Ugly, but does work for me in the end. I have not gained any encyclopaedic knowledge of the MS Network Client and its config - I have merely found my way through all the weeds.
And, I threw an optional dis_pkt9 in the mix - to allow CRYNWR-compatible apps as well, if desired. That ZIP does not contain the KB1049 update, mentioned in many of the tutorials (a bugfix to the NET.EXE and some companion inline help files).
The ZIP file on the link above contains instructions in README.TXT files. Please complain to me at [ Frantisek DOT Rysanek AT post DOT cz ] if you find this file offending in any way.
The DOS Network client 3.0 installation disks can still be found around the interwebs, if you dig deep enough and you're interested enough.

Alternatively, it is possible to use a slightly more modern and feature-packed edition of the Microsoft stack: the Lan Manager 2.2a or 2.2c.

To start a Microsoft network client in DOS, you need a couple things: the name of a Workgroup (or domain), the name and IP address of your server, possibly names of the "shares" you are interested in (if you know them in advance), and a login and password for the server.

Apparently, you need to specify a mapping between hostanames and IP addresses in the lmhosts file, in order to be able to map shares via "Microsoft hostnames". I've tried via IP addresses and via DNS names... no way. You need to have LanMan names working. I.e., one lmhosts entry for each server that you need to access shares on.
Feel free to experiment with WINS in Samba, perhaps you can make it work for DOS, perhaps it depends on the version/flavour of the MS network client for DOS. DNS alone doesn't seem to cut it, maybe if you tell Samba to relay WINS to DNS etc.

Note that I did my experiments in a "workgroup" environment, i.e. not a proper NT domain.

My "stripped-down NetBootDisk cut" of the MS Network Client has the Workgroup stored in system.ini. Several binaries get loaded in a generic way from "start.bat". The interesting stuff actually happens in "logon.bat" : there's a call to "NET LOGON", followed by two lines with "NET USE", mapping an example disk share and an example printer share ((C) Brad Driver, the author of NetBootDisk):

net logon %USERNAME% %PASSWORD% /yes /savepw:no
net use G: \\%SERVER%\%HDDSHARE% %PASSWORD%
net use LPT2: \\%SERVER%\%PRINTQUEUE% %PASSWORD%

The references to E1000.DOS can be found in some .INI files too. There are some additional entries copied from the Vendor-original INF files of the E1000 driver...

Actually, rather than bother with my kludgey ZIP download, perhaps you should give the original NetBootDisk a try. It makes a lot of magic happen ... well ... automagically :-) especially with respect to HW-specific drivers (auto-detected and auto-loaded). There's a documented way to add your own drivers, or to replace older driver .DOS binaries with updated versions (done this with Intel, Realtek, Marvell, Atheros and many others).

The official installers of MS Network Client 3.0 and MS LanMan 2.2 will guide you through the setup using a simple full-screen text-mode wizard = if you get your HW-specific NDIS driver in the right "format" (I'm hinting at the INF/INI files, and grinning+winking at Intel) then it's all pretty seamless. Check the References for a video walkthrough on YouTube.

Useful DOS-side network commands to "probe your environment":

NET VIEW
NET VIEW \\SERVERNAME
NET USE
NET HELP
NET HELP USE 

And... how do you print? How do you test that the DOS side works?
Well... you can just copy some flat ASCII text file to the printer soft-device:

copy sometext.txt LPT2

Alternatively, you can use some app, such as a text editor, to create the print job, and send it to LPT2 natively. The condition is, that the app must use the DOS or BIOS API for the printing, rather than direct HW access.

Either way, if the rest of the printing subsystem has been configured, you should see your job coming out of the printer. Or at least, you should see your client PC making an effort towards the Samba server - as observed by Wireshark on the LAN.

Samba on the server

Samba is an open-source implementation of the Microsoft LAN networking protocol suite, on top of TCP/IP transport (no more NetBEUI, thanks god). Thus, it is also a natural counterpart to the Microsoft Network client for DOS / the LAN Manager.
That said, note that the Microsoft LanMan/SMB/CIFS protocol suite has evolved for a long time and has gone a long way. There are in fact
several generations of the protocol, and mutual compatibility between clients and servers of different generations is not a sure thing. More precisely, both modern Windows and modern Samba can still speak LanMan1 to clients, but the capability is nowadays turned off by default in the server's configuration, due to security concerns. Frankly, the old SMB protocol flavours are using an outright silly authentication concept, unfit for a modern company LAN. Then again, behind a firewall, in an isolated SoHo environment, or a small lab setup, where you are your own boss... who cares, right :-)

What goes into smb.conf

Samba is a beast. A swiss army knife, trying to cater for a very wide spectrum of Microsoft protocol versions.
There are heaps of configurable options - many of them old and officially scorned, but still functional.

The following is an example smb.conf, quite minimalistic:

# The ordering of the options doesn't seem to matter much,
# but it's probably a good idea to specify the more general options
# higher up in smb.conf, and more specific / dependent options below.

[global]
	server max protocol = SMB3
	server min protocol = CORE 
	client min protocol = CORE 
 
#client code page=850
dos charset = CP850
mangling method = hash
unix charset = UTF-8

#create mask = 0777
create mask = 0775
#create mode = 0755
#force create mode = 0755

directory mask = 0775
#directory mode = 0755
#force directory mode = 0755

# this was a default in Samba 3.6 and older:
acl allow execute always
# Allows you to run .EXE files straight from a network share, in Windows.

# workgroup = NT-Domain-Name or Workgroup-Name
   workgroup = atmyhome

# server string is the equivalent of the NT Description field
   server string = File and print server

# This option is important for security. It allows you to restrict
# connections to machines which are on your local network. The
# following example restricts access to two C class networks and
# the "loopback" interface. For more examples of the syntax see
# the smb.conf man page
;   hosts allow = 192.168.1. 192.168.2. 127.
# Before you try restricting access to your Samba based on IP 
# addresses, I suggest that you first make it work without IP address 
# restrictions. There are other things that can potentially go wrong in 
# the config, and it's useful not to make the riddle unnecessarily 
# harder.


# When setting up printing via Samba, it is probably easiest
# to let Samba learn about the printers on its own.
# The printers discovered will be automatically turned into SMB printing shares.

   load printers = yes

# Samba can ask the local system for a list of printers installed,
# in several different ways - which has to do with the several different
# printing API's available.
# Perhaps the most important printing style's / API's nowadays are
# CUPS and the legacy LPR/LPD interface.

# A) legacy LPR/LPD printing
;   printing = bsd 
;   printcap name = /etc/printcap
# The printcap is a characteristic config file of the LPD/LPR printing system,
# also used by apps that need printing, to learn about the list of printers.

# B) native CUPS printing
   printing = cups
   printcap name = /run/cups/printcap
# not sure if the printcap is needed by Samba when using CUPS... 
# On part of CUPS, the printcap is just a shim for old apps 
# that still expect the LPD/LPR API. Samba should be able to use the
# CUPS library (libcups) to get a list of printers directly.

# Smbd warns in the log, if it's looking for an /etc/printcap and hasn't 
# found one.

# The list of printing systems, currently supported by Samba, include:
# bsd, sysv, plp, lprng, aix, hpux, qnx, cups
# In theory, you should not need to specify "printing =",
# the API should get auto-detected.



# Uncomment this if you want a guest account, you must add this to /etc/passwd
# otherwise the user "nobody" is used
;  guest account = pcguest

# This tells Samba to use a separate log file for each machine
# that connects:
   log file = /var/log/samba/%m.log
# Uncomment this to get a conveniently elevated log:
;   log level = 3 acls:5 rpc_srv:5 auth:4

# Put a cap on the size of the log files (in Kb).
   max log size = 0

# Security mode. Most people will want user level security. See
# security_level.txt for details.
   security = user
   username map script = /bin/echo

# LAN Manager = archaic DOS era auth
# NTLM = NT Lan Manager
   lanman auth = yes
   ntlm auth = yes
   server role = standalone server

# You may wish to use password encryption. Please read
# ENCRYPTION.txt, Win95.txt and WinNT.txt in the Samba documentation.
# Do not enable this option unless you have read those documents
#   encrypt passwords = true
#   smb passwd file = /etc/samba/smbpasswd
#   passdb backend = smbpasswd
   passdb backend = tdbsam:/etc/samba/passdb.tdb

# The following is needed to keep smbclient from spouting spurious errors
# when Samba is built with support for SSL.
;   ssl CA certFile = /usr/share/ssl/certs/ca-bundle.crt

# The following are needed to allow password changing from Windows to
# update the Linux system password also.
# NOTE: Use these with 'encrypt passwords' and 'smb passwd file' above.
# NOTE2: You do NOT need these to allow workstations to change only
#        the encrypted SMB passwords. They allow the Unix password
#        to be kept in sync with the SMB password.
   unix password sync = Yes
   passwd program = /usr/bin/passwd %u
   passwd chat = *New*password* %n\n *Retype*new*password* %n\n *passwd:*all*authentication*tokens*updated*successfully*

# You can use PAM's password change control flag for Samba. If
# enabled, then PAM will be used for password changes when requested
# by an SMB client instead of the program listed in passwd program.
# It should be possible to enable this without changing your passwd
# chat parameter for most setups.

   pam password change = yes

# Unix users can map to different SMB User names
;  username map = /etc/samba/smbusers

# Using the following line enables you to customise your configuration
# on a per machine basis. The %m gets replaced with the netbios name
# of the machine that is connecting
;   include = /etc/samba/smb.conf.%m

# This parameter will control whether or not Samba should obey PAM's
# account and session management directives. The default behavior is
# to use PAM for clear text authentication only and to ignore any
# account or session management. Note that Samba always ignores PAM
# for authentication in the case of encrypt passwords = yes

  obey pam restrictions = yes

# The recommended and default socket options have evolved
# through the years... feel free to tweak this to achieve better
# performance - but don't expect much effect against DOS-based clients.
# The defaults should be okay for a start (achieve a working system).
# See speed.txt and the manual pages for details.
#   socket options = TCP_NODELAY SO_RCVBUF=8192 SO_SNDBUF=8192
#   socket options = TCP_NODELAY SO_RCVBUF=262144 SO_SNDBUF=262144
#   read size = 524288
#   write size = 524288
#   socket options = TCP_NODELAY SO_RCVBUF=32768 SO_SNDBUF=32768
   socket options = TCP_NODELAY SO_REUSEADDR
#   read size = 65536
#   write size = 65536
#   read prediction = yes

# Configure Samba to use multiple interfaces
# If you have multiple network interfaces then you may wish to list them
# here. See the man page for details.
;   interfaces = 192.168.12.2/24 192.168.13.2/24 


# The following several options are related to local and cross-subnet
# browsing (between different servers in a workgroup environment):

# Configure remote browse list synchronisation here
#  request announcement to, or browse list sync from
#	a specific host or from / to a whole subnet (see below)
;   remote browse sync = 192.168.3.25 192.168.5.255

# Cause this host to announce itself to local subnets here
;   remote announce = 192.168.1.255 192.168.2.44

# Browser Control Options:
# set local master to no if you don't want Samba to become a master
# browser on your network. Otherwise the normal election rules apply
   local master = yes

# OS Level determines the precedence of this server in master browser
# elections. The default value should be reasonable
   os level = 65

# Domain Master specifies Samba to be the Domain Master Browser. This
# allows Samba to collate browse lists between subnets. Don't use this
# if you already have a Windows NT domain controller doing this job
   domain master = yes 

# Preferred Master causes Samba to force a local browser election on startup
# and gives it a slightly higher chance of winning the election
   preferred master = yes


# Enable this if you want Samba to be a domain logon server for 
# Windows95 workstations. 
   domain logons = yes

# if you enable domain logons then you may want a per-machine or
# per user logon script
# run a specific logon batch file per workstation (machine)
;   logon script = %m.bat
# run a specific logon batch file per username
;   logon script = %U.bat

# Windows Internet Name Serving Support Section:
# WINS Support - Tells the NMBD component of Samba to enable it's WINS Server
   wins support = yes

# WINS Server - Tells the NMBD components of Samba to be a WINS Client
#	Note: Samba can be either a WINS Server, or a WINS Client, but NOT both
;   wins server = w.x.y.z

# WINS Proxy - Tells Samba to answer name resolution queries on
# behalf of a non WINS capable client, for this to work there must be
# at least one	WINS Server on the network. The default is NO.
#   wins proxy = yes

# DNS Proxy - tells Samba whether or not to try to resolve NetBIOS names
# via DNS nslookups. The built-in default for versions 1.9.17 is yes,
# this has been changed in version 1.9.18 to no.
   dns proxy = no 
#   dns proxy = yes 

# Case Preservation can be handy - system default is _no_
# NOTE: These can be set on a per share basis
;  preserve case = no
;  short preserve case = no
# Default case is normally upper case for all DOS files
;  default case = lower
# Be very careful with case sensitivity - it can break things!
;  case sensitive = no

# The following has to do with user ID's.
# Mapping is specifically needed when mixing local UNIX user ID's
# and "remote" accounts (LDAP, AD and the like).
# Unless you have a specific problem related to this, do not touch.
idmap config * : range = 1000-1999999
#idmap config * : range = 500-1999999

#============================ Share Definitions ==============================

# NOTE: If you have a BSD-style print system there is no need to 
# specifically define each individual printer
[printers]
   comment = Imported UNIX system printers
   path = /var/spool/samba
   browseable = no
   public = yes 
# to allow user 'guest account' to print
   guest ok = yes
   writable = no
   printable = yes

[SAMBA]
   path = /var/test/samba
   create mask = 0666
   directory mask = 0777
   store dos attributes = yes
   public = no
   only guest = no
   writable = yes
   printable = no
   dos filenames = yes
   mangle names = yes
   mangling method = hash
   mangle prefix = 6
   dos filetimes = yes
   write list = frank
   read list = frank
   valid users = frank

The way the user accounts are configured in my example config, Samba needs each user configured in the local system (passwd+shadow) and in addition you need to tell Samba about the user by means of
smbpasswd -a <username>

As for smb.conf, Samba actually has many more configuration parameters, with reasonable default values. The Samba package contains a companion utility called "testparm" that can shed light on these. To get a complete listing of config parameters and their values, i.e. those specifically mentioned in smb.conf and those omitted/implicit, use:
testparm -v

The one thing that I'd like to specifically "leave up to other sources", is the privilege system and its various flavours, and the several magnificent security modes. There is share-level security and four different modes of user-level security, and there are Windows ACL's that can be mapped onto Linux/POSIX ACLs (backed by XATTRs) etc.

Suffice to say that I've spent a day or two last year, trying to build a sane privilege system for a particular directory tree on a Samba server, trying to meet some pretty specific requirements of a particular boss inside our company... I took some time to inspect the user-level privileges and ACL's, i played with setfacl/getfacl and xattr... only to conclude that A) the mapping from Windows doesn't work very well, and B) there are very practical downsides that hamper my efforts to build a friendship with the POSIX ACL stuff.
I ended up just using basic user-level security, specifying the read+write privileges per user groups. Privileges mapped in smb.conf to users/groups per share, user groups defined in /etc/group. This style of privilege configuration is old and traditional for Samba, and it turns out to work really quite well, in spite of being overall scorned and deprecated.

The Samba printing bug biting DOS

In the future, as new releases of Samba seep into newer releases of the various Linux distroes, this problem will possibly become a non-issue (and hopefully the compatibility with the DOS client won't get broken in some violent and deliberate way).

At the time of this writing (summer 2021), most distroes contain builds of Samba that have a funny bug in handling print jobs from DOS clients (the LanMan1 protocol generation). Superficially, the bug expresses the following symptoms: when you send a print job to Samba from a DOS client, the job will get truncated after just over 100 Bytes. The Samba server fails to emit some response message, expected by the client - and after a noticeable timeout, the printing session gets canceled, which may end up with an error message on the client, and maybe a partial page printed on the printer.

The bug has been fixed in Samba releases 4.13.8 and 4.14.5.

A silly workaround to the Samba+DOS printing bug

As a side note, let me mention a possible workaround to the printing bug - a particular way of getting a file printed by copying it to a file share, where it gets snatched by a server-side process and printed. The point is to avoid the broken SMB printing functionality, by using a file share instead.
The arrangement is kind of awkward, but I'd like to mention it nonetheless, for the record - it's a snippet of classic UNIX-style tinkering and it may inspire people to try other hacks along these lines.

So how do we go about it:

Put a UNIX-domain named pipe in the Samba share, locally on the server:

	cd /var/test/samba
	mknod print.q p
	chmod 666 print.q

The above needs to run as user root.
The following can run as an unprivileged user.

To test the idea (dry run):

	while true; do cat print.q; echo "\n Another one:"; done

To actually run the print jobs though the printer back end:

	while true; do lpr -P MYPRNQUEUE < print.q; done

In the DOS client, having a drive G: mapped to the network share, I can do this (either works):

	echo "Howdy :-)" >> G:\print.q
	type autoexec.bat >> G:\print.q
	copy /Y /B autoexec.bat G:\print.q

Before trying this, I was a little worried that especially the "copy" would erase the pipe and replace it with a plain file - that did not happen, the pipe still exists and functions.

The named pipe provides a clear job start and job end, inherently, by closing the pipe to the "consumer" process (lpr) when the "producer" has closed his file copy/append operation. In our case, the "producer" runs in DOS, and sends output to the pipe via the CIFS/SMB network filesystem.

Note that after some time, lpr times out waiting for data (and exits, and gets respawned). This is normal and probably harmless. As far as I could observe, an lpr waiting for a print job to come does not "block the CUPS queue" in any way.

Compile Samba from source?

If you can't wait to print from DOS to Samba, and the binary updates have not reached the distro repos yet, you may want to compile Samba from source. The following is an approximate recipe, distilled in Ubuntu 20.04 in the summer of 2021. Let me know if my recipe is missing some of the dependencies - likely something very basic that I had in my system from previous occasions.

mkdir /usr/src
cd /usr/src
wget https://download.samba.org/pub/samba/stable/samba-4.14.5.tar.gz
ls -l
tar tvzf samba-4.14.5.tar.gz 
tar xvzf samba-4.14.5.tar.gz 
cd samba-4.14.5

# you can stay with apt-get if you're not a fan of aptitude...
#apt-get install aptitude
aptitude update
aptitude install gcc make binutils bison flex libtool m4 autoconf automake pkg-config python3-distutils python3-dev liblmdb-dev gnutls-dev libgpgme11-dev libparse-yapp-perl libjansson-dev libarchive-dev libacl1-dev libldap2-dev libpam-dev libdbus-1-dev libtasn1-bin libcups2-dev libsystemd-dev libfam-dev xsltproc

# FYI:
#./configure --help | less
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-fhs --enable-cups --with-systemd --systemd-install-services
# vi bin/config.log 

# The -j2 tells make to utilize up to 2 threads (otherwise runs single-threaded).
# Feel free to specify more threads if you have more CPU cores.
make -j2

# /etc/samba/smb.conf should actually survive the reinstallation,
# but we should make a backup for a good measure
cp /etc/samba/smb.conf /root/
aptitude remove samba

make -j2 install

# the following is needed to prevent a funny error symptom,
# where you're able to "net use" disk shares, but any attempt
# to "net use" a printer share results in "DOS error 67: the network name cannot be found".
# In the station's server-side smb log, this shows up as 
# "canonicalize_connect_path failed for service prntest, path /var/spool/samba"
mkdir /var/spool/samba
chmod 777 /var/spool/samba

# surprisingly, /usr/lib/samba need not be in here...
echo "/usr/lib" > /etc/ld.so.conf.d/samba.conf
ldconfig

systemctl enable nmb
systemctl start nmb
systemctl status nmb

systemctl enable smb
systemctl start smb
systemctl status smb

Samba tips and tricks

smbclient -U frank -L //myserver
smbstatus
systemctl status smbd
journalctl -u smbd -b
testparm -v

CUPS

CUPS intro

Cups is a printing subsystem for UNIX - popular in Linux and BSD, apparently also present in MacOS X, and in Solaris. CUPS is a modern successor to the traditional UNIX print spoolers lpr/lpd and lprng. While the original spoolers would merely "store and forward" print jobs to the destination devices (printers) with some optional filtering, CUPS has somewhat higher ambitions: to provide a unified printing interface for UNIX applications, and to separate the application interface / data format from the printer-specific print job formats. Apparently, for a long time, automagical on-the-fly conversion of print job formats (PDL's) has been the key asset of CUPS. Which, in the context of our "case study", is useful to arrange the format conversion necessary for printing from old DOS tools to modern printers.
Lately, an even more advanced ambition on part of CUPS is, to do away with printer model specific drivers (filters) - apparently by standardizing on yet another set of PDL's, and by calling that system "driver-less(TM)". The transport protocol is possibly the easier part :-)

CUPS inbound print job interface

The CUPS daemon listens on TCP port 631, speaking its own protocol called the Internet Printing Protocol (or IPP) - clearly based on HTTP as the underlying "transport layer", with a framework of print-specific rules / arrangements / standardized paths riding on top of raw HTTP. This is how CUPS can accept print jobs, respond to configuration requests, queue management requests, and at the same time serve a human-friendly HTTP GUI, all on TCP port 631.
Using a companion tool called cups-lpd (to be started from inetd), the CUPS daemon can also accept print jobs using the legacy LPR protocol.

CUPS on-the-fly filters

The filters should pretty much work "out of the box", you do not need to tackle their configuration "first thing in the morning".
This chapter is only here because it belongs here semantically - but, the best thing for you to do upon the first reading of this paper is perhaps to skip this chapter. The topic is relatively "advanced". Make a mental remark that this area exists and return to it if relevant to you.

In the old days of the LPR protocol / lpd spooler daemon, print jobs got forwarded "as is" by default. For your particuar printer, you needed a matching printer driver on the client machines. When I met Linux/UNIX, lpr already could be configured to use filters (hooked into config via /etc/printcap), among them format conversion filters - and there were even some magical auto-filters, for hipsters who were so inclined.
CUPS has turned the auto-smart-filtering into a core principle of its operation.

As already mentioned, CUPS tries to convert all the jobs internally to a common format: PostScript. This is done by first-stage filters, structured by the "MIME type" of the data (print job) received on input. This is what I'd like to deal with in this chapter.
As a second stage, CUPS also has output filters, which adapt the uniform print job for a particular destination printer = they work as "HW-specific printer drivers". This is where
PPD's come into play (we have some further details on that in a later chapter). And, there's a broad plethora of these "output filters", for pretty much any odd printer format out there, including many modern-day "GDI" printer families, including "driverless" printers with their modern/simple/unified formats (CUPS/PWG/Apple-Raster, PCLm etc). Many "output filters" (and apparently a separate PPD generator) are maintained out of the upstream CUPS tree / by third parties and Linux distroes...

So the first-stage "filters" are in fact a host of executable programs (or scripts), structured by their input format: text-to-ps, pdf-to-ps and suchlike. The configuration of input filtering holds together by two classic config files: mime.types and mime.convs.
In my random version of Ubuntu, these live in /usr/share/cups/mime - but overrides and additions can possibly exist elsewhere, such as under /etc/ (local.convs). I've seen complaints that local.convs do not apply - if you want to be sure that your changes do apply, maybe just hack the primary files in the systemwide directory :-( [sigh]

mime.types defines various detection and search expressions, mapping to a particular data format. The "MIME type" concept comes from elsewhere, you may know it from various sorts of internet software (SMTP e-mail and HTTP web) - CUPS has reused it for its own purposes and AFAICT, CUPS deals with MIME types at its own terms, has its own definitions / store. The purpose of the mime.types file is to tell CUPS "if you find XYZ in the job data, this is an XYZ file type".

mime.convs takes the previously determined MIME type and maps a particular filter program to it. The actual filter can be some stand-alone binary program with arbitrary command-line arguments (such as GhostScript) - and it is fairly common that the "filter" referred to by mime.convs is a mere wrapper script, adapting cmdline arguments for the actual binary.
In my random version of Ubuntu, the CUPS filter binaries and wrapper scripts are all collected in /usr/lib/cups/filter/ .

Curiously to me, the CUPS input filters are traditionally somewhat incomplete. Out of the box, CUPS can certainly recognize and pre-convert into PostScript several input formats: TXT, PDF, various bitmap picture formats, and of course PS itself. But, it does not pre-convert HP PCL or Epson ESC*P. Such popular print job formats - why is that?
If you go looking into mime.convs and mime.types, you'll notice that mime.types does contain a detection sequence for PCL and ESC*P, but both these formats are swept into a common category called "raw" - which happens to be a bypass route, that does not get converted into PS, but rather, gets forwarded to the destination printer without any mangling. So that, if you have e.g. a fine well-behaved Epson printer, and a Windows machine with a corresponding Epson ESC*P2 driver, you will get your print jobs printed through cups, without ever knowing that the sweet CUPS magic has not in fact taken place.
Hmm... such a violation of the CUPS core principle! Such a waste of effort...

Now in our scenario with DOS as the printing client, support for format conversion in CUPS can be a key requirement, to have the whole "signal chain" work! In the old days of DOS, there was no "printing subsystem" as a software layer or API. Your only printing API was the Centronics / Intel 8255 LPT port hardware, possibly wrapped in a thin BIOS service or the DOS file-copy API. As for the printer data format, that was "left up to the user". Your application program needed to produce the right data format to please your particular printer.

In reality, in the old days of MS-DOS, among the most popular printer brands and print job formats were: HP PCL (possibly v3, later v5) and Epson ESC*P/ESC*P2. PostScript was also known, and available as an output generator in some application software, but PS-capable printers were the snob end. There were other popular printer makers back then, such as Star and IBM, whose proprietary formats were however not as popular in the MS-DOS application software.
Back then, the lowest common denominator was plain ASCII, including some basic non-visual control characters (CR, LF, FF). Pretty much any dot-matrix and most PCL-capable laser printers could be relied on to print basic ASCII just fine - and the early Inkjets were just as well. Modulo some twists and quirks around national language support (alternative EPROM's, or the option to upload fonts at runtime, etc.) The point is: as soon as you wanted the printer to work in raster graphics mode, whether to print bitmap/vector graphics or just some nice WYSIWYG text with proportional fonts, rendered by the PC software, that's where you needed a HW-specific printer data format (print job language).

So... to provide good support for DOS printing, including raster output, your best bet probably is to make your printing back-end accept HP PCL. The Epson ESC*P/P2 family would be nice too, but in general not required (unless you have a specific need in that vein).
If you have a choice, HP PCL is perhaps the more modern format of the two, being page-oriented and supporting the "nice" resolutions of 300/600/1200 dpi, in contrast to the line-oriented Epson format, combined with the awkward resolution base of 180/360/720 dpi.

Speaking of HP PCL and CUPS filters: interestingly enough, the ubiquitous GhostScript interpreter (a dependency of CUPS) has a sibling called GhostPCL = an interpreter of the HP PCL printing language family, capable of output to PostScript and several raster formats. Curiously enough, GhostPCL is not included with CUPS. If you want GhostPCL, you need to compile it from source, and add it to your CUPS configuration.

In the public interwebs, this has been pioneered and documented by Maximillian Dornseif in the CUPS mailing list (other people have helped him correct a typo in his config).
That mailinglist thread occurred back in 2007. Today in 2021, and for the record, let me paraphrase/recap/reinterpret his configuration thus (not tested on my part):

Compile GhostPCL from source, and install in your system.
You will want to create a wrapper script, called e.g. pcltopdf - drop this in /usr/lib/cups/filter, /usr/bin or somewhere else based on personal preference. The script might look something like:

#!/bin/sh
# (C) 2007 Maximillian Dornseif
[ -n "$6" ] && exec <"$6"
/usr/bin/pcl6  -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=- -

...which can be tested at the command line e.g. by:
pcltopdf < test.pcl > test.pdf

In mime.types, you need to disable = comment out the original "bait for PCL" that points to application/vnd.cups-raw, and add your own mime type specifically for HP PCL - i.e. something like:
application/vnd.hp-PCL          string(0,<1B>E<1B>)
#application/vnd.cups-raw       (string(0,<1B>E) + !string(2,<1B>%0B)) \
[...etc...]

In mime.convs, you need to add an entry, connected to your custom new MIME type:
application/vnd.hp-PCL  application/pdf 65      pcltopdf

...and finally, restart CUPS.

If you can find a plausible software interpreter of Epson ESC*P2, it should be easy to make this work for the Epson format too.

CUPS outbound print job transport

In the outbound direction, CUPS can pass its print jobs onto the printers (or further intermediate print servers) using one of several physical layers and protocols:

These different URI's get catered for by binary helper executables called "cups backends".

The ipp:// and usb:// URI's can actually be quite arcane - can contain UID's, MAC addresses etc. If interested, consult official CUPS documentation...

Hostnames vs. IP addresses in URI's

In CUPS printer URI's, the "host" part = the target machine, can be specified in one of two ways:

  1. by an IP address, or
  2. by a corresponding name, which will get translated ("resolved") into an actual IP address behind the scenes.

CUPS is running in UNIX/Linux, so the obviously relevant name system here is the DNS. (Forget about the sambaesque NetBios names and WINS at this point.) So obviously you can use DNS names, that are somehow centrally managed for your network: ISC BIND, MS AD or whatnot. If you're a sysadmin of traditional upbringing, this is likely your preferred way of referring to the machines in your network. But, that is not the end of your options.

DNS lives in your network - but, within your local machine running CUPS, DNS is just one of several "name resolution methods" available to your local system's resolver library. Another classic is /etc/hosts = a file where you can define name-to-IP mappings local to your machine. Yet, even that is not the end of your options.

Related to printing and IPP, you will notice another name resolution system: the Apple Bonjour's very own mDNS/DNS-SD. You will probably notice "ephemeral" printers popping up in your system, with URI format along the lines of dnssd://HP%20LaserJet%204050%20Series._pdl-datastream._tcp.local  . Here, the   "HP LaserJet 4050 Series"   looks like a self-generated local hostname, but the whole "name" ending in ._tcp.local is actually a "fully qualified service name". Managed within the local Ethernet in an automagical / distributed fashion by Bonjour / mDNS + DNS-SD.
The "hostname" is actually advertised by the printer machine, and in some networked printers I've seen the Bonjour name configurable in the printer's own HTTP-based config interface.
Apparently, e.g. Ubuntu has the mDNS client incorporated in the resolver library.

In general, I can but respect the Bonjour names and URI's in the ephemeral printers popping up on my machines in the LAN, but I prefer to use my own DNS-managed names for permanent printer profiles that I configure explicitly, by hand.

CUPS printers: static vs. PnP

As mentioned in this very nice manpage about printers.conf, the set of printers reported by CUPS can be split into two major categories:

  1. printers that are locally configured using the config tools. These get saved into printers.conf and typically get a PPD file copied to /etc/cups/ppd/. We have a dedicated chapter on the CUPS config files just below. A statically configured printer gets a persistent local queue, from where jobs do not vanish even if the printer is momentarily unavailable. Queued jobs get printed once the printer becomes physically present again.
  2. printers that merely "flutter around within direct eyesight" (layer 2 broadcast domain). Ephemeral printers, appearing via PnP mechanisms. (The dedicated chapter has further details.) These do not get saved in printers.conf! Yet they are printable, while visible via CUPS or Bonjour/Avahi. These are printers with a full-fledged support of the IPP protocol, and as such, they can disclose enough information about the page format, resolution etc., so that a dedicated PPD is not necessary. They are also typically "driverless" as well = they use one of the modern apple-flavoured PDL's, and hence do not need a dedicated format-conversion filter/driver.
    Still, you may wish to "nail down" some such printer - to prevent it from "fluttering around", to have its profile permanently visible on your computer, to give it a queue name of your own choice, and maybe a Destination URI of your own choice. Read on for details how to configure a printer in a static fashion.

The CUPS built-in HTTP GUI doesn't seem to have a button called "nail down this ephemeral printer to make it static", but you can go through the "Administration" page, "Add Printer" and pick one of the listed "Discovered Network Printers". Earlier at the Administration page, there's also a button called "Find New Printers" - not sure what use that one is. Perhaps to search for printers that are not so vocal via spurious broadcasts.
Alternatively, you can check the ephemeral printer's properties, maybe make a note of the URI and other parameters via the clipboard if interested (e.g. if you're not fluent enough in the URI format and know-how to roll your own) and then configure a "new" printer manually, pointing the "add printer" wizard to the printer you already know about, in the database (or by uploading a PPD of your own).
Note that there are also command-line tools to scan for printers, that dump useful components of a possible URI, and to configure a static printer profile in your CUPS.

While trying to "nail down an ephemeral printer" to make it locally/statically configured, and after some fiddling with its config, you may end up in a situation where the "nailed down" profile becomes so distinct from what Bonjour had suggested automatically, that CUPS will then report the auto-detected version again as a yet another distinct ephemeral printer :-)
To avoid this, you can either use the Bonjouresque URI and queue name in your static config verbatim (to make CUPS and friends understand that this is just one and the same printer), or you can instead try to disable the PnP printer detection altogether. Also note that decent networked printers have a management GUI (often via HTTP) where you can configure the "Bonjour name" that gets reflected as the ephemeral CUPS printer's name.

By "fluttering around" when referring to the ephemeral PnP printers, I certainly mean the fact that printers keep coming and going of their own discretion, but also, that the PnP does not always respond instantly. In certain situations you may get a printer appear late (the printer does not pop up instantly when in fact it's already within reach and alive) and the ephemeral printer profiles may not vanish from sight immediately when the printer gets detached from the network or shut down.

CUPS vs. Samba Vs. DOS: choice of queue name

Speaking of DOS interoperability, an important motivation to use a particular/different local name for your CUPS printer profile (=queue) is the fact that the profile name, as advertised by CUPS, gets automatically imported by SAMBA, maybe mangled a bit, and re-exported as a Samba printer share name. Modulo some dashes and underscores and undocumented rules on share name length, and havoc can arise when mapping the shares in poor old DOS. This is mentioned again in the following chapter on CUPS config files.

For this reason and purpose = for DOS, I'd suggest to stick to CUPS printer queue names that are 8 letters long and contain no dashes and underscores. Also, as CUPS doesn't seem to support queue alias names (in contrast to LPD/LPRNG), you may want to set up a "shim" or "proxy" printer profile within CUPS, to have an alternative "shorthand queue name for DOS" for an existing printer profile where you'd like to stick to some longer "primary" name obtained from Bonjour or some such. As a result of which, you get two printer shares advertised by samba, one unusable in DOS, and another one looking redundant to users in Windows etc. There's no single solution to suit everybody and look nice.

CUPS configuration files

Site-specific config of the cups service/daemon lives in text files under /etc/cups/ . Two key config files are /etc/cups/cupsd.conf and /etc/cups/printers.conf - and individual printer profiles from printers.conf link to individual PPD files in /etc/cups/ppd/ .
Global config is stored in cupsd.conf, and apparently CUPS does not modify that file. Printer queues, defined in /etc/cups/printers.conf, are a different matter. This file gets updated by the CUPS daemon while running, based on interaction through the
config tools.

Before I go ahead with my wall of text, let me try and put the various tidbits in a picture:

The PPD files contain various printer model-specific features and attributes: a list of standard paper sizes supported, their non-printable areas (borders), maybe information about built-in fonts, resolutions, some other stuff... A key snippet od data in a CUPS PPD is a reference to a "filter" = on-the-fly PDL format conversion program.

While studying the config for the case at hand, I kept wondering, how does CUPS know what PPD file to load, for a particular printer entry from /etc/cups/printers.conf. There is no "PPD file" attribute in there! And the answer is: the link is established by making the PPD filename (without suffix) equal to the particular print queue name. (I.e., if your PPD files in /etc/cups/ppd/ are named after printer model, that's pure coincidence, based on your own choice of queue name! unless of course the queue name has been generated by CUPS for you...) For clarity, these "implicit links" are emphasized by rainbow colors in my picture above. These "printer profile names" do matter in other respects too! These are the queue names exported into printcap and re-advertised via cups-lpd (if used) and Samba.

Where does the printer profile name come from? Some GUI tools (those easily accessible) do not even allow you to enter the print queue name of your choice. Instead, the queue name ends up being a lengthy string, learned from the printer - taken from the printer's factory-default Bonjour configuration or something. (A decent LAN-attached printer will allow you to configure its Bonjour name, if you care.) On your CUPS machine, if you don't find the right option to enter the queue name in the available GUI-based printer config tools, you can edit the printer profile name (queue name) by hand in /etc/cups/printers.conf - and rename the PPD in /etc/cups/ppd/, if already there. See the next-but-one paragraph.
Probably the most appropriate interface to use for adding a static printer is the built-in HTTP of CUPS - allowing you to enter the desired queue name.

CUPS in its current version takes the PPD's seriously. A printer profile that does not have a PPD assigned, often does not accept print jobs. There's one exception: raw printers - these do not use a PPD.
You may have noticed that CUPS has a database of known printer models. For instance in Debian, this comes from a package called openprinting-ppds, whose major content is a file called /usr/lib/cups/driver/openprinting-ppds, which is a 6+MB Python source file (script), apparently containing an inlined archive with all the PPD's gzipped and base64 encoded. YUCK! But ahh well - this is the actual source of where these PPD's come from.
All in all, in order to get your PPD cleanly and easily in place, in the current CUPS version you really should go through the HTTP GUI, add a new printer and then select a vendor and model of the printer, which selects the corresponding PPD file "behind the scenes" (and copies+renames the PPD file into /etc/cups/ppd).
Alternatively, ephemeral IPP-capable printers probably provide CUPS with a PPD of their own (upon submitting every single print job).
Alternatively, you can provide CUPS with a PPD file of your own - upload the file through the HTTP GUI. Just if you choose "raw" from the list of vendors and then "raw" from the list of printers, no PPD gets copied. (I suspect that the "raw flag" is stored in the "Type 4" attribute in printers.conf.)

So: it can sometimes be useful to modify /etc/cups/printers.conf by hand. Such as, to insert a different Destination URI, or to change the queue name. Before you do that, be sure to stop the CUPS service:
systemctl stop cups
... edit printers.conf ...
systemctl start cups
Why stop the service before modifying /etc/cups/printers.conf? This is because, if CUPS configuration got changed via the config tools available, while the cupsd was running, these changes get saved by cups on cups shutdown! Therefore, if you'd modify the printers.conf, and then you'd just do a systemctl restart cups, your changes done directly in printers.conf would get lost.

As already mentioned, the ephemeral printers do not get saved into /etc/cups/printers.conf. If you would nonetheless like to make one of those printers "permanently configured", you can do that in some of the config tools by asking the tool to create a profile for the printer, and that it should look for network printers that could serve as a basis. Such as, in the built-in HTTP GUI of CUPS, click "Administration" in the top menu bar, "Add Printer", and select your printer among "Discovered Network Printers".
You can then modify the profile thus created to your liking, you can probably snatch its PPD from /etc/cups/ppd for your own pointless joy etc. Note that if you change the static profile, deviating from the autoselected queue name and URI, your printer will probably re-appear as another ephemeral entry :-)

When choosing a print profile=queue name, and in the context of this "case at hand", if you are aiming for printing from DOS via Samba, you'd better choose a short and simple queue name. I suggest 8 characters max (although 11 letters might work too), and just in case, you'd better avoid dashes and underscores. Other sources suggest 32 letters max for a share name, but those were not specific to DOS.

While running, cupsd creates and keeps up to date a "printcap" file for legacy compatibility. This one is not in /etc (as was usual with the older printing spooler daemons). Instead, CUPS places it in /var/run/cups/printcap. This makes sense: /etc/ is a directory for static configuration. And /var/run/cups/printcap can receive changes pretty often. If need be, make a symlink from /etc/printcap to /var/run/cups/printcap.

CUPS configuration tools

CUPS itself provides a comprehensive config tool, in the form of its built-in GUI on port 631. Click the following link to proceed to the CUPS management interface on the machine where you're reading this HTML text:
http://localhost:631/ in this tab
http://localhost:631/ in a newly open tab

Linux distroes typically provide GUI tools for CUPS configuration. Such as the system-config-printer, or the Ubuntese "configuration - printing" applet in the Unity desktop. Sadly, these nice GUI tools are not exhaustive.

CUPS also comes packaged with a set of nice command-line tools, such as lpadmin and lpstat.

Based on experience, at the time of this writing, for manual configuration of printers I'd suggest the built-in HTTP based management interface of CUPS, over the native GUI tools of your OS/desktop. This makes good sense in that the built-in HTTP interface of CUPS gets developed as part of CUPS and chances are that it is therefore in sync with any developments deeper under the hood. It also seems the most feature-complete.

Printer autoconfig by Avahi/Bonjour and IPP

The ways of PnP are not always straightforward, and the GUI tools are not entirely free of bugs. The PnP-style auto-detection of printers in a LAN, while pretty much a treat in a SoHo environment, quickly turns into a major PITA in a larger company LAN. With heaps of printers popping up and vanishing all the time in your OS'es printing dialog, it's a nuissance to find the right one, which is often the one and only printer in your physical office... I.e. in a more "managed" environment you want to configure your printers explicitly, and weed out the pesky funky sprites fluttering around.

In other words: if you'd prefer to configure print queues by hand, or have them fixed after the first auto-detection, you'd probably like to prevent PnP from configuring the printers.

It appears that there are two mechanisms in this play:

Your local CUPS subsystem interacts with both of them, and to avoid the ephemeral printer sprites, you need to address them both.
Each gets a dedicated sub-chapter in the following text.

Please take the following observations with a grain of salt, as a "work in progress". I have yet to find out, exactly what components in the system speak Bonjour and IPP and keep bringing the ephemeral printers in the picture. And, find sensitive ways of telling them *not to*, without impairing other desirable functions in the system, especially the desktop environments. Feedback welcome.

Blocking Avahi

Avahi is an open-source implementation of the Apple Bonjour protocol suite / configuration mechanism. Further keywords include dnssd, zeroconf, and dbus in the local operating system. Avahi is implemented in the form of an avahi-daemon, that keeps running in the system, listening to the network traffic and issuing local event messages through DBUS to various applications that are interested to get notified of different things.

The avahi-daemon and dbus are definitely relevant to the GUI config tools. If you want your IPP-compliant networked printers detected, start the printer config GUI applet available in your system, and maybe power-cycle the printer if it doesn't appear all by itself. (Not all printers announce themselves via Bonjour... that alone does not mean that you won't be able to send jobs to such an Apple-agnostic printer.)

If you want to avoid the printing PnP mess, and stick to printers configured explicitly, you will want to prevent Avahi from intervening in printing. Unfortunately, it seems that some parts of the system (and especially some desktop apps) depend on the avahi-daemon being active.
Someone has
suggested a gentle way. of how to prevent Avahi from messing with the network printer profiles all the time, while avahi itsef remains operational, so that applications don't time out waiting for it:

Open /etc/avahi/avahi-daemon.conf and modify the following two lines:
change “use-ipv4=yes” to “use-ipv4=no”
change “use-ipv6=yes” to “use-ipv6=no”

In my observations within Ubuntu 20.04 LTS, this trick alone does not get rid of the ephemeral networked printers fluttering around in your printing-related GUI dialogs - and, it would be untrue to claim that this is in some way better than a simple
systemctl stop avahi-daemon
...because in my observation, after the aforementioned configuration mod, on the next startup the avahi-daemon immediately exits with an error message, that it has all communication protocols disabled and therefore doesn't have a reason to live.

Blocking CUPS-Browsed

Note that CUPS comes with its own PnP chatter daemon, called cups-browsed. Apparently, this one's got its own implementation of Bonjour. And, apart from Bonjour, the cups-browsed also listens for CUPS-native (IPP) broadcasts/announcements, and cups-browsed itself advertises local CUPS queues into the network via the CUPS protocol.

To prevent cups-browsed from scanning the LAN for printers and making them all available, you can find a few suggested approaches:

  1. systemctl stop cups-browsed
    systemctl disable cups-browsed
    Reportedly, this does *not* do the trick on modern Ubuntu versions (19 and later), not sure exactly why. Maybe Avahi works around this workaround, or CUPS itself can now scan the network, or what.
  2. Apparently, you can also tell cups-browsed to avoid scanning for printers, via its configuration file: /etc/cups/cups-browsed.conf. To achieve the desired effect, try one of the following:
    BrowseProtocols none
    BrowseRemoteProtocols none
  3. Others suggest the following addition to /etc/cups/cupsd.conf -- i.e. to instruct the main CUPS daemon itself:
    Browsing Off
    BrowseLocalProtocols none

Apparently the options vary across distroes and through evolution in time.

Also note that decent networked printers allow you to turn individual printing interfaces on/off individually (Bonjour, IPP, JetDirect, LPD). Which is another way to suppress the PnP clutter - albeit principally susceptible to any printer, connected to the LAN in factory default settings, popping up on everyone's desktop :-)

CUPS tips and tricks

(ppeveprinter is likely irrelevant)
ippfind
avahi-browse -a
avahi-browse -rt _ipp._tcp

# Set up a simple "local loopback alias" CUPS queue via the command line:
lpadmin -p HPLJ6 -E -v ipp://localhost/printers/HP_LaserJet_6_series

lpstat -a
lpq -a

Debugging tips

If things are not going to plan, you may need an elevated log level here and there, or intercept the print job along the "transmission chain" and check how it fares.

The following is a list of things to be aware of:

While wrangling the DOS net client, the full-fledged GUI Wireshark can come in surprisingly handy. Or if you don't have GUI (X) on the Samba server, you can always use tcpdump or T-Shark to capture to a file (.pcap/.pcapng) at the command line, and view the capture file offline on some graphical desktop. Or run a "remote live capture stub" on the server, if you want a headless capture with real-time GUI analysis.
Long story short, Wireshark understands the SMB/CIFS protocol and can give you a nice "cascaded tree" human-legible parsed view of the protocol transactions.
So that, when "NET USE LPT2: \\SERVER\PRINTERSHARE" responds with something like "error 69: share name not found" (which is its universal error response), you can go "ahaa, the client did actually ask my Samba for this share, and Samba did actually respond 'no such thing here, so sod off' ".

After which, you go check the Samba access log file, and you see "ahaa, Samba has tried creating a file in the configured spool directory, and that directory does not in fact exist." -- and in the next iteration you see "ahaa, Samba has tried creating a file in the configured spool directory, which now exists, but failed to create the file due to insufficient privileges, in spite of the fact that smbd itself is reported as running under the "root" user - what ho, does the servant child run with privileges of the logged in user by any chance? Whatever, 'chmod 777 /var/spool/samba' and yippee, it works :-)

References = further reading and downloads


by: Frank Rysanek [rysanek AT fccps DOT cz]
in 2021