I finally got a new printer. After my lexmark CX510 printer had issue after issue with printing (paper jam, no paper alarm etc.) I decided it is time for a new printer.
So which one should I buy? I was quite happy with the linux support for my lexmark printer, but not with the printer itself. I had an epson and brother printer before and they worked fine but in the end I was sorting my candidates by price and availability. The brother HL-L2375 won. On paper these are the features:

  • print speeds of up to 34 pages per minute
  • Automatic 2-sided print
  • 250 sheet paper Input
  • Built in wired and wireless connectivity
  • Up to 1,200 page* inbox toner
  • 64MB memory

So, an OK printer (IMHO). There are linux drivers on the homepage and someone on amazon actually said it worked out of the box without further printer driver install. Yeah..no

Anyway, I was hoping so and bought it. It obviously did not work out of the box. I needed the driver.

I already looked at the nixpkgs search for brother printer and knew, the driver wasn’t ported over, yet. So lets do it:

I first went to the brother homepage and downloaded the driver .deb file. The directory structure is

brother-printer/
├── control
│   ├── control
│   ├── md5sums
│   ├── postinst
│   ├── postrm
│   └── prerm
├── control.tar.gz
├── data
│   ├── etc
│   │   └── opt
│   │       └── brother
│   │           └── Printers
│   │               └── HLL2375DW
│   │                   └── inf
│   ├── opt
│   │   └── brother
│   │       └── Printers
│   │           └── HLL2375DW
│   │               ├── cupswrapper
│   │               │   ├── brother-HLL2375DW-cups-en.ppd
│   │               │   ├── Copying
│   │               │   ├── lpdwrapper
│   │               │   ├── lpdwrapper_own
│   │               │   └── paperconfigml2
│   │               ├── inf
│   │               │   ├── brHLL2375DWfunc
│   │               │   ├── brHLL2375DWrc
│   │               │   └── setupPrintcap
│   │               ├── LICENSE_ENG.txt
│   │               ├── LICENSE_JPN.txt
│   │               └── lpd
│   │                   ├── armv7l
│   │                   │   ├── brprintconflsr3
│   │                   │   └── rawtobr3
│   │                   ├── i686
│   │                   │   ├── brprintconflsr3
│   │                   │   └── rawtobr3
│   │                   ├── lpdfilter
│   │                   └── x86_64
│   │                       ├── brprintconflsr3
│   │                       └── rawtobr3
│   ├── usr
│   │   └── share
│   │       └── doc
│   └── var
│       └── spool
│           └── lpd
│               └── HLL2375DW
├── data.tar.gz
└── debian-binary

Lots of empty directories. When we concentrate on the opt directory, we can see three directories. cupswrapper, inf and lpd. The last one branches itself in three architectures armv7l, i686 and x86_64.

We obviously need the brother-HLL2375DW-cups-en.ppd file. In it we find the following code:

*cupsFilter:    "application/vnd.cups-postscript 0 brother_lpdwrapper_HLL2375DW"
*cupsFilter:    "application/vnd.cups-pdf 0 brother_lpdwrapper_HLL2375DW"

So, we need a cupsFilter. Where does this come from? No file is named this way. So lets have a look at the debian control files. This is the content of postinst which gets executed when installing the package on a debian system:

postinst
#!/bin/sh
if [ "$(echo $(uname -m) | grep -i 'arm')" != '' ]; then
  ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
elif [ "$(echo $(uname -m) | grep -i -e 'x86_64' -e 'amd64')" != '' ]; then
  ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
else
  ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
fi

if [ -e /etc/init.d/lprng ]; then
     /opt/brother/Printers/HLL2375DW/inf/setupPrintcap HLL2375DW -i USB
     /etc/init.d/lprng restart
elif [ -e /etc/init.d/lpd ]; then
     /opt/brother/Printers/HLL2375DW/inf/setupPrintcap HLL2375DW -i USB
     /etc/init.d/lpd restart
fi
if [ ! -e /usr/sbin/pstops ];then
 PSTOPS=`which pstops 2>/dev/null`
 if [ "`echo $PSTOPS | grep -i cups`" != "" ];then
  PSTOPS=""
 fi
fi

ln -s /opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc       /etc/opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc

if [ ! -e /usr/bin/brprintconflsr3_HLL2375DW ];then
 echo "#! /bin/sh"  > /usr/bin/brprintconflsr3_HLL2375DW
 echo "/opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3 -P HLL2375DW" '$''*'           >>/usr/bin/brprintconflsr3_HLL2375DW
  chmod 755 /usr/bin/brprintconflsr3_HLL2375DW
fi

if [ ! -e /usr/bin/perl ] && [ "`which perl`" != ''  ];then
  if [ -e "`which perl`" ];then
    echo ln -s "`which perl`"  /usr/bin/perl
    ln -s "`which perl`"  /usr/bin/perl
  fi
fi
if [ ! -e /usr/bin/perl ]; then
  echo ' ****** WARNING: /usr/bin/perl is required. ******'
fi


if [ ! -e /usr/bin/perl ] && [ "`which perl`" != ''  ];then
  if [ -e "`which perl`" ];then
    echo ln -s "`which perl`"  /usr/bin/perl
    ln -s "`which perl`"  /usr/bin/perl
  fi
fi
if [ ! -e /usr/bin/perl ]; then
  echo ' ****** WARNING: /usr/bin/perl is required. ******'
fi


if [ -e /usr/lib/cups/filter ] &&    [ ! -e /usr/lib/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib32/cups/filter ] &&    [ ! -e /usr/lib32/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib32/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib64/cups/filter ] &&    [ ! -e /usr/lib64/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib64/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/libexec/cups/filter ] &&    [ ! -e /usr/libexec/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/libexec/cups/filter/brother_lpdwrapper_HLL2375DW
fi

if [ -e /usr/share/cups/model ];then
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/brother-HLL2375DW-cups-en.ppd      /usr/share/cups/model
  PPDDIR=/usr/share/cups/model/
fi
if [ -e /usr/share/ppd ];then
  if [ ! -e /usr/share/ppd/brother ];then
    mkdir /usr/share/ppd/brother
  fi
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/brother-HLL2375DW-cups-en.ppd      /usr/share/ppd/brother
  PPDDIR=/usr/share/ppd/brother/
fi

if [ "$(which lpinfo  2> /dev/null)" != '' ];then
  uris=$(lpinfo -v)

  for uri in $uris
  do  
    URI=$(echo $uri | grep "HL-L2375DW" | grep usb)
    if [ "$URI" != '' ];then
      break;
    fi
  done

  if [ "$URI" = '' ];then
    for uri in $uris
    do  
      URI=$(echo $uri | grep "HL-L2375DW")
      if [ "$URI" != '' ];then
        break;
      fi
    done
  fi

  if [ "$URI" = '' ];then
    for uri in $uris
    do
      URI=$(echo $uri | grep -i "Brother" | grep usb)
      if [ "$URI" != '' ];then
        break;
      fi
    done
  fi
  if [ "$URI" = '' ];then
    for uri in $uris
    do
      URI=$(echo $uri | grep usb)
      if [ "$URI" != '' ];then
        break;
      fi
    done
  fi

  if [ "$URI" = '' ];then
    URI="usb://dev/usb/lp0"
  fi

  if [ "$(which lpadmin  2> /dev/null)" != '' ];then
    echo lpadmin -p HLL2375DW  -E -v $URI -P ${PPDDIR}brother-HLL2375DW-cups-en.ppd
    lpadmin -p HLL2375DW -E -v $URI -P ${PPDDIR}brother-HLL2375DW-cups-en.ppd
  fi
fi

if [ "$(which semanage 2> /dev/null)" != '' ];then
  semanage fcontext -a -t cupsd_rw_etc_t '/etc/opt/brother/Printers/HLL2375DW/inf(/.*)?'
  semanage fcontext -a -t cupsd_rw_etc_t '/opt/brother/Printers/HLL2375DW/inf(/.*)?'
  semanage fcontext -a -t bin_t          '/opt/brother/Printers/HLL2375DW/lpd(/.*)?'
  semanage fcontext -a -t bin_t          '/opt/brother/Printers/HLL2375DW/cupswrapper(/.*)?'

  if [ "$(which restorecon  2> /dev/null)" != '' ];then
    restorecon -R /opt/brother/Printers/HLL2375DW
    restorecon -R /etc/opt/brother/Printers/HLL2375DW
  fi
fi

We can break it down:

#!/bin/sh
if [ "$(echo $(uname -m) | grep -i 'arm')" != '' ]; then
  ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
elif [ "$(echo $(uname -m) | grep -i -e 'x86_64' -e 'amd64')" != '' ]; then
  ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
else
  ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/rawtobr3         /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
  ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/brprintconflsr3         /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
fi

This links the executables rawtobr3 and brprintconflsr3 depending on the architecture. These are propitiatory binary files. We’ll come back to them later.

The next section just restarts the services after setting up Printercap (not really relevant here).

ln -s /opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc       /etc/opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc

This links the rc file from opt to etc. Also not really interessting, although the file will come in handy later on.

if [ ! -e /usr/bin/brprintconflsr3_HLL2375DW ];then
 echo "#! /bin/sh"  > /usr/bin/brprintconflsr3_HLL2375DW
 echo "/opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3 -P HLL2375DW" '$''*'           >>/usr/bin/brprintconflsr3_HLL2375DW
  chmod 755 /usr/bin/brprintconflsr3_HLL2375DW
fi

This creates a script which calls brprintconflsr3 with -P HLL2375DW $*. So it seems brprintconflsr3 needs to know which printer it uses.

The next section just checks for perl to be available. Since the scripts are perlscripts, we obviously need it.

f [ -e /usr/lib/cups/filter ] &&    [ ! -e /usr/lib/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib32/cups/filter ] &&    [ ! -e /usr/lib32/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib32/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib64/cups/filter ] &&    [ ! -e /usr/lib64/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/lib64/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/libexec/cups/filter ] &&    [ ! -e /usr/libexec/cups/filter/lpdwrapper ];then 
  ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      /usr/libexec/cups/filter/brother_lpdwrapper_HLL2375DW
fi

Ah, now we finally come across our brother_lpdwrapper_HLL2375DW file, which our PPD needs. So the lpdwrapper (perl) is our file. This part justs links it to the correct directory.

The next part just copies the PPD file to the correct directory.

The next part sets up the printer with lpadmin and the correct URI. Also not relevant to the driver itself. The last part finally takes care of SELinux, which we don’t need to get involved in, either.

So, to sum it up:

  1. Link binary files rawtobr3 and brprintconflsr3 to the system directories
  2. Link rc file to the system directories
  3. Create startup script brprintconflsr3_HLL2375DW which just calls brprintconflsr3 with -P HLL2375DW
  4. Link lpdwrapper to brother_lpdwrapper_HLL2375DW

So, lets have a look at the lpdwrapper file:

lpdwrapper
#! /usr/bin/perl

[...]

#   log functions

$LOGFILE="/tmp/br_cupswrapper_ml2.log";
$LOGLEVEL=7;
$DEBUG=0;
$LOG_LATESTONLY=1;
$DEVICEURILOCK=1;
$LPD_DEBUG=0;

if ( $DEBUG > 0 ){
  $LPD_DEBUG=2;
}

[...]

$LPDCONFIGEXE="brprintconflsr3";

my $INPUT_PS   = "/tmp/br_cupswrapper_ml2_input.ps";
my $OUTPUT_PRN = "/tmp/br_cupswrapper_ml2_output.prn";


#    main

logprint( 0 , "START\n");


$ENV{OWNER} = $ARGV[1];
$ENV{TITLE} = $ARGV[2];

$ENV{NODENAME} = `uname -n`;


my $basedir = Cwd::realpath ($0);
if ( $basedir eq '' ){
  $basedir = `readlink $0`;
  if ( $basedir eq '' ){
      $basedir = `realpath $0`;
  }
}
chomp($basedir);

$basedir =~ s/\/cupswrapper\/.*$//g;



my $cmdoptions=$ARGV[4];
my $PPD = $ENV{PPD};
my $CUPSINPUT='';
if ( @ARGV >= 6 ){
    $CUPSINPUT=$ARGV[7];
}

my $PRINTER=$basedir;
$PRINTER =~ s/^\/opt\/.*\/Printers\///g;
$PRINTER =~ s/\/cupswrapper.*$//g;
$PRINTER =~ s/\///g;

for  (my $i = 0 ; $i < @ARGV ; $i ++){
    logprint( 0 , "ARG$i      = $ARGV[$i]\n");
}

logprint( 0 , "PRINTER   = $PRINTER \n");
logprint( 0 , "PPD       = $PPD\n");
logprint( 0 , "BASEPATH  = $basedir\n");

logprint( 0 , "export PPD=$PPD\n");  
logprint( 0 , "$0 \"$ARGV[1]\"  \"$ARGV[2]\"  \"$ARGV[3]\"  \"$ARGV[4]\"  \"$ARGV[5]\"  \"$ARGV[6]\"\n");  


my $LPDFILTER   =$basedir."/lpd/lpdfilter";

logprint( 0 , "\n");

[...] 

my $LATESTINFO="/tmp/".$PRINTER."_latest_print_info";
unlink $LATESTINFO;
`touch $LATESTINFO`;


my $TEMPRC = "/tmp/br".$PRINTER."rc_".$$;

`cp  $basedir/inf/br${PRINTER}rc  $TEMPRC`;
$ENV{BRPRINTERRCFILE} = $TEMPRC;

logprint( 0 , "TEMPRC    = $TEMPRC\n");

$LOCKFILE="/tmp/$PRINTER"."_lf_".$ENV{DEVICE_URI};

if ( $DEVICEURILOCK == 1){
   open (FILE , "+> $LOCKFILE");
   flock(FILE , 2);
}


$ENV{LPD_DEBUG} = $LPD_DEBUG;
$ENV{PS}=1;

$ENV{BRPAPERWIDTH}  = $width;
$ENV{BRPAPERHEIGHT} = $height;

[...]

&exec_lpdconfig ( $basedir ,$PRINTER , \%lpr_options );

    logprint( 2, "\n");
if ( $DEBUG == 0 ){
    $command = "cat $CUPSINPUT |  $LPDFILTER";
    logprint( 2 , "$command\n");
    system("$command");
}

[...]

elsif ($DEBUG > 1 ){
    $command = "cat $CUPSINPUT > $INPUT_PS && cat $INPUT_PS |".
	       "$LPDFILTER > $OUTPUT_PRN";

    logprint( 2,  "export BRPAPERWIDTH=$ENV{BRPAPERWIDTH}\n");
    logprint( 2,  "export BRPAPERHEIGHT=$ENV{BRPAPERHEIGHT}\n");
    logprint( 2,  "export PPD=$ENV{PPD}\n");
    logprint( 2,  "export BRPRINTERRCFILE=$LATESTINFO\n");
    logprint( 2,  "export LPD_DEBUG=$ENV{LPD_DEBUG}\n");
    logprint( 2,  "export PS=$ENV{PS}\n");

    logprint( 2, "cat $INPUT_PS | $LPDFILTER > $OUTPUT_PRN \n");
    system("$command 2> /tmp/br_cupswrapper_ml2_lpderr");
    print "\0";

}


`mv  "$TEMPRC"   "$LATESTINFO"`;
`echo "\n\nCUSTOM PAGE SIZE ${width}x${height}" >> $LATESTINFO`;
#unlink  $TEMPRC;

[...]

exit 0;

#-----------------------------------------------------------

[...]

#exec lpd config

sub exec_lpdconfig {
    (my $basedir , my $PRINTER , my $lpr_options_ref) =  @_;

    my $lpddir = $basedir;
    my %lpr_options = %$lpr_options_ref;

    $lpddir = $basedir."/lpd/";
    my $lpdconf = $lpddir.'/'.$LPDCONFIGEXE;

    
    while(($op , $val) = each(%lpr_options)){
        my $lpdconf_command = "$lpdconf -P $PRINTER $op $val";
	logprint( 0 ,   "$lpdconf_command\n");
	`$lpdconf_command`;
    }

}

I cut some stuff out. Its a long file. It should still give you a hint what it does. First, we see it works a lot with temporarly files. Either to log, or, more importantly, to setup the printer. Also some path are hardcoded. One of the more interessting lines is the following:

my $TEMPRC = "/tmp/br".$PRINTER."rc_".$$;

`cp  $basedir/inf/br${PRINTER}rc  $TEMPRC`;
$ENV{BRPRINTERRCFILE} = $TEMPRC;

logprint( 0 , "TEMPRC    = $TEMPRC\n");

This creates a /tmp/br.HLL2375DW.rc_1234 variable, where the numbers are random (I think). Then the rc file from above gets copied to this destination. This gets exported as an environment variable BRPRINTERRCFILE. Later we can see the line

$command = "cat $CUPSINPUT |  $LPDFILTER";
logprint( 2 , "$command\n");
    system("$command");

The $CUPSINPUT is one of the arguments the lpdwrapper file is called with from cups. See https://www.cups.org/doc/api-filter.html for more infos. So, what is the $LODFILTER variable? Its my $LPDFILTER =$basedir."/lpd/lpdfilter"; So, wait.. CUPS calls the PPD, which calls this script, which calls..the next script?

Unfortunatly, it gets even worse. The lpdfilter script calls the proprirotory pre-compiled binaries to set more options of the printer. But, look for yourself:

lpdfilter
#! /usr/bin/perl
#
# LPD/LPRng filter                                  ver 2.01

[...]

my $basedir = Cwd::realpath ($0);
if ( $basedir eq '' ){
  $basedir = `readlink $0`;
  if ( $basedir eq '' ){
      $basedir = `realpath $0`;
  }
}
chomp($basedir);
$basedir =~ s/\/lpd\/.*$//g;

my $PRINTER=$basedir;
$PRINTER =~ s/^\/opt\/.*\/Printers\///g;
$PRINTER =~ s/\/lpd\/.*$//g;
$PRINTER =~ s/\///g;

my $INPUT_TEMP='';
my $FILE_TYPE="PostScript";
my $LOGFILE = "/tmp/br_lpdfilter_ml2.log";

$LOG_FIRSTTIME = 1;
$LOGLEVEL = 7;
$DEBUG = $ENV{LPD_DEBUG};

[...]

my $BR_PRT_PATH = $basedir;

my $RCFILE=$ENV{BRPRINTERRCFILE};
if ( $RCFILE eq '' ){
 $RCFILE=sprintf ("$BR_PRT_PATH/inf/br%src",$PRINTER);  
}
$FUNCFILE=sprintf ("$BR_PRT_PATH/inf/br%sfunc",$PRINTER);  
$FLAG = `grep 'flags1='  $FUNCFILE | sed s/'flags1='//g`;
chomp($FLAG);
if ( $FLAG eq '' ){
    $FLAG="0000000000000002";
}

$offset = `grep 'offset='  $FUNCFILE | sed s/'offset='//g`;
chomp($offset);

my $BRCONV="$BR_PRT_PATH/lpd/rawtobr3";
my $BRCONV_OP="-rc $RCFILE -flags $FLAG -offset $offset";

[...]

if ( $ENV{PS} ne '1' ){
    $INPUT_TEMP=`mktemp /tmp/br_input.XXXXXX`;
    chomp($INPUT_TEMP);
    `cat > $INPUT_TEMP`;
    $FILE_TYPE=`file $INPUT_TEMP`;
    $FILE_TYPE=~ s/^.*:[ ]*//;
    $FILE_TYPE=~ s/[ ].*//;
    if ( $DEBUG ne '0' ){
	copy "$INPUT_TEMP"      ,   "/tmp/br_lpdfilter_ml2_input.ps";
    }
}
else{
    $INPUT_TEMP='';
    $FILE_TYPE="PostScript";

    if ( $DEBUG ne '0' ){

	$INPUT_TEMP=`mktemp /tmp/br_input.XXXXXX`;
	chomp($INPUT_TEMP);
	`cat > $INPUT_TEMP`;
	copy "$INPUT_TEMP"     ,    "/tmp/br_lpdfilter_ml2_input.ps";
    }

}

logprint (1,  "PRINTER=$PRINTER");
logprint (1,  "\$ENV{PS} =  $ENV{PS}" );
logprint (1,  "\$ENV{BRPRINTERRCFILE} = $ENV{BRPRINTERRCFILE}");

my $paper = "A4";
my $resolution = "600";

open (FPRCFILE , $RCFILE); 

my $rcline ;
while ($rcline = <FPRCFILE>){
    if ( $rcline =~ /Resolution/){
	$resolution = $rcline;
	$resolution =~ s/Resolution=//;
	chomp($resolution);
    }
    elsif ( $rcline =~ /PaperType/){
	$papertype =  $rcline;
	$papertype =~ s/PaperType=//;
	chomp($papertype);
    }
}
close(FPRCFILE);

$width =  $ENV{BRPAPERWIDTH};
$height = $ENV{BRPAPERHEIGHT};

logprint(1, "\$ENV{BRPAPERWIDTH} = $ENV{BRPAPERWIDTH}");
logprint(1, "\$ENV{BRPAPERHEIGHT} = $ENV{BRPAPERHEIGHT}");
my $size_br = '';

if ( $width eq '' || $height eq '' || 
     $width == 0  || $height == 0  ||
     $width == -1 || $height == -1  ){
  my $paperref = $PAPERTBL{$papertype};
  $width  = $paperref->{width};
  $height = $paperref->{height};
  logprint(1, " TYPE=$papertype  w=$width h=$height size_br=$size_br \n");
}

$size_br = " -ps ${width}x${height}";
$BRCONV_OP .= $size_br;

[...]

my $GHOST_SCRIPT=`which gs`;
chomp($GHOST_SCRIPT);

my $OUTPUT_TYPE="bit";
my $GHOST_OPT="-q -dNOPROMPT -dNOPAUSE -dSAFER -sDEVICE=$OUTPUT_TYPE -sstdout=%stderr -sOutputFile=- - -c quit";

my $gscommand = "";
if ( $HWMARGINS eq "yes" ){
  $gscommand = 
      "(echo  '<</.HWMargins[12. 12. 12. 12.]>>setpagedevice';".
      "cat $INPUT_TEMP)" .
      " | $GHOST_SCRIPT -r$resolution -g$size_gs $GHOST_OPT ";
}
else{
  $gscommand = 
    "cat $INPUT_TEMP | $GHOST_SCRIPT -r$resolution -g$size_gs $GHOST_OPT";
}

my $brcommand="$BRCONV $BRCONV_OP";

if ( $DEBUG eq '1' ){
    system("$gscommand | $brcommand");
    logprint( 0, "$gscommand | $brcommand") ;
}
elsif ( $DEBUG eq '2' ){
    `$gscommand     > /tmp/br_lpdfilter_ml2_gsout.dat`;
    `cat /tmp/br_lpdfilter_ml2_gsout.dat | $brcommand >/tmp/br_lpdfilter_ml2_out.prn`;
    system("cat /tmp/br_lpdfilter_ml2_out.prn");


    `cp  $RCFILE /tmp/br_lpdfilter_ml2.rc`;
    $brcommand="$BRCONV -rc /tmp/br_lpdfilter_ml2.rc -flags $FLAG -offset $offset $size_br";

    logprint(1, "$gscommand ".
	        "> /tmp/br_lpdfilter_ml2_gsout.dat");
    logprint(1, "cat /tmp/br_lpdfilter_ml2_gsout.dat | $brcommand".
                ">/tmp/br_lpdfilter_ml2_out.prn\n");
    logprint(1, "cat /tmp/br_lpdfilter_ml2_out.prn ");


}
else{
    system("$gscommand | $brcommand");
}

if ( $INPUT_TEMP ne '' ){
    unlink $INPUT_TEMP;
}

exit 0;

So, to make it short: It opens the file from the environment variable BRPRINTERRCFILE, gets the values from it and calls gs with -q -dNOPROMPT -dNOPAUSE -dSAFER -sDEVICE=$OUTPUT_TYPE -sstdout=%stderr -sOutputFile=- - -c quit and rawtobr3 with -rc $RCFILE -flags $FLAG -offset $offset.

So far, so good. We now have a little bit of an overview of how this driver works. We now need to get this into NixOS. Fortunatly, someone already packaged a different, but very similiar brother printer driver: https://github.com/NixOS/nixpkgs/pull/165514

{ lib
, stdenv
, fetchurl
, dpkg
, autoPatchelfHook
, makeWrapper
, perl
, gnused
, ghostscript
, file
, coreutils
, gnugrep
, which
}:

let
  arches = [ "x86_64" "i686" "armv7l" ];

  runtimeDeps = [
    ghostscript
    file
    gnused
    gnugrep
    coreutils
    which
  ];
in

stdenv.mkDerivation rec {
  pname = "cups-brother-mfcl2750dw";
  version = "4.0.0-1";

  nativeBuildInputs = [ dpkg makeWrapper autoPatchelfHook ];
  buildInputs = [ perl ];

  dontUnpack = true;

  src = fetchurl {
    url = "https://download.brother.com/welcome/dlf103566/mfcl2750dwpdrv-${version}.i386.deb";
    hash = "sha256-3uDwzLQTF8r1tsGZ7ChGhk4ryQmVsZYdUaj9eFaC0jc=";
  };

  installPhase = ''
    runHook preInstall
    mkdir -p $out
    dpkg-deb -x $src $out
    # delete unnecessary files for the current architecture
  '' + lib.concatMapStrings (arch: ''
    echo Deleting files for ${arch}
    rm -r "$out/opt/brother/Printers/MFCL2750DW/lpd/${arch}"
  '') (builtins.filter (arch: arch != stdenv.hostPlatform.linuxArch) arches) + ''
      # bundled scripts don't understand the arch subdirectories for some reason
      ln -s \
        "$out/opt/brother/Printers/MFCL2750DW/lpd/${stdenv.hostPlatform.linuxArch}/"* \
        "$out/opt/brother/Printers/MFCL2750DW/lpd/"
      # Fix global references and replace auto discovery mechanism with hardcoded values
      substituteInPlace $out/opt/brother/Printers/MFCL2750DW/lpd/lpdfilter \
        --replace /opt "$out/opt" \
        --replace "my \$BR_PRT_PATH =" "my \$BR_PRT_PATH = \"$out/opt/brother/Printers/MFCL2750DW\"; #" \
        --replace "PRINTER =~" "PRINTER = \"MFCL2750DW\"; #"
      # Make sure all executables have the necessary runtime dependencies available
      find "$out" -executable -and -type f | while read file; do
        wrapProgram "$file" --prefix PATH : "${lib.makeBinPath runtimeDeps}"
      done
      # Symlink filter and ppd into a location where CUPS will discover it
      mkdir -p $out/lib/cups/filter
      mkdir -p $out/share/cups/model
      ln -s \
        $out/opt/brother/Printers/MFCL2750DW/lpd/lpdfilter \
        $out/lib/cups/filter/brother_lpdwrapper_MFCL2750DW
      ln -s \
        $out/opt/brother/Printers/MFCL2750DW/cupswrapper/brother-MFCL2750DW-cups-en.ppd \
        $out/share/cups/model/
      runHook postInstall
    '';

  meta = with lib; {
    homepage = "http://www.brother.com/";
    description = "Brother MFC-L2750DW printer driver";
    license = licenses.unfree;
    platforms = builtins.map (arch: "${arch}-linux") arches;
    maintainers = [ maintainers.lovesegfault ];
  };
}

I’m not going to much into details here, as lovesegfault (the author) commented everything pretty good. I initially just adapted the file to use the correct source and changed the printer model. This was all that needed to be done! It worked! Yay!

Well, it did print.

It did not print two-sided, in different resolutions or different paper sizes. Whats going on?

Well, looking at the above scripts, we notice that the L2750DW printer only uses the lpdfilter file. But we don’t just have that, but also the lpdwrapper, right?

The workflow seems to be:

CUPS –> PPD –> lpdwrapper –> lpdfilter –> ghostscript, own binaries

So, although it works, no settings are conveied from CUPS to the printer. Lets debug it:

First, I installed the .deb file on a native Ubuntu install. There, everything worked. So it must be something in the way I packaged the driver. Digging a bit deeper, I can see the content of the RCFILE on Ubuntu (which is under /tmp):

[HLL2375DW]
Language=LANG_USA
Resolution=600
PaperSource=Tray1
OutputBin=Auto
Duplex=OFF
DuplexType=Long
PaperType=A4
Media=PlainPaper
Copies=1
Sleep=PrinterDefault
TonerSaveMode=OFF


CUSTOM PAGE SIZE {Undefined}x{Undefined}

and on NixOS:

[HLL2375DW]
Language=LANG_USA
Resolution=600
PaperSource=Tray1
Duplex=OFF
DuplexType=Long
PaperType=A4
Media=PlainPaper
Copies=1
Sleep=PrinterDefault
TonerSaveMode=OFF

The NixOS file is exactly the same as the original one from the inf directory, wheras the ubuntu one has some custom options in it. So apparently, this is the reason it didn’t convey the infos..

As we could see above, there is a DEBUG flag in the perl scripts. How handy! Settings this to 2 will produce a few more files in the /tmp directory that we can debug. (Please note: On NixOS those files are under /tmp/systemd-hash-cups and are private to the process. You need to be root to access those).

When doing so, we can see: (br_cupswrapper_ml2_log)

START
ARG0      = 6
ARG1      = florian
ARG2      = Wikipedia
ARG3      = 1
ARG4      = PageSize=A4 EPRendering=None XRXColor=BW INK=MONO SelectColor=Grayscale HPColorMode=GrayscalePrint ProcessColorModel=Mono BRMonoColor=Mono CNGrayscale PrintoutMode=Normal.Gray BRPrintQuality=Black Collate Duplex=None ColorMode=Mono CNIJGrayScale=1 ARCMode=CMBW ColorModel=Gray OKControl=Gray XROutputColor=PrintAsGrayscale BLW=TrueM job-uuid=urn:uuid:70973965-f3d4-391e-52e7-ee87b516fb8a job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1671024720 time-at-processing=1671024720
PRINTER   = HLL2375DW 
PPD       = /etc/cups/ppd/HLL2375DW.ppd
BASEPATH  = /opt/brother/Printers/HLL2375DW
export PPD=/etc/cups/ppd/HLL2375DW.ppd
/usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW "florian"  "Wikipedia"  "1"  "PageSize=A4 EPRendering=None XRXColor=BW INK=MONO SelectColor=Grayscale HPColorMode=GrayscalePrint ProcessColorModel=Mono BRMonoColor=Mono CNGrayscale PrintoutMode=Normal.Gray BRPrintQuality=Black Collate Duplex=None ColorMode=Mono CNIJGrayScale=1 ARCMode=CMBW ColorModel=Gray OKControl=Gray XROutputColor=PrintAsGrayscale BLW=TrueM job-uuid=urn:uuid:70973965-f3d4-391e-52e7-ee87b516fb8a job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1671024720 time-at-processing=1671024720"  ""  ""

TEMPRC    = /tmp/brHLL2375DWrc_3310

SET PPD OPTIONS
-ts  <=  OFF  : (OFF)
-res  <=  600  : (600dpi)
-pt  <=  A4  : (A4)
-sp  <=  PRINTER  : (PrinterDefault)
-md  <=  PLAIN  : (PLAIN)
-ps  <=  T1  : (TRAY1)
-dx  <=  ON  : (DuplexNoTumble)
-dxt  <=  LONG  : (DuplexNoTumble)

SET VENDOR COMMAND OPTIONS

SET PPD CMD OPTIONS
-dx  <=  OFF  : (None)
-pt  <=  A4  : (A4)

SET VENDOR NUMERIC COMMAND OPTIONS

SET MEDIA (STANDARD) COMMAND OPTIONS
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -sp PRINTER
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -dxt LONG
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -cp 1
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ps T1
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -res 600
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ts OFF
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -pt A4
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -md PLAIN
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -dx OFF

export BRPAPERWIDTH=-1
export BRPAPERHEIGHT=-1
export PPD=/etc/cups/ppd/HLL2375DW.ppd
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
export LPD_DEBUG=2
export PS=1
cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn 

and br_cupswrapper_ml2_lpderr (on ubuntu, this file is empty):

Invalid output bin -1

Well. Thats not helpful.

When calling the last line cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn (or /nix/store/hash-cups-brother-hll2375dw-4.0.0-1/opt/brother/Printers/HLL2375DW/lpd/lpdfilter on NixOS) we get an error about cannot open file. Remeber the environment settings? Yes, right, so lets try:

export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn 
Invalid output bin -1

Now we can reproduce the error Invalid output bin -1. Isn’t that great?

We already took a look at the RC file. You’ll note the line OutputBin=Auto in the ubuntu installation, but not in the NixOS one. Why?

Well, we come back to that. First, lets try whether we can get rid of the error when manually adding the line to the file (This file is identical to the RCFILE, because the script will copy the RCFILE to HLL2375DW_latest_print_info after the print job is done):

 echo "OutputBin=Auto" >> /tmp/HLL2375DW_latest_print_info
 export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
 cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn 
 # no output

Yay, it worked!

Ok, so why the heck doesn’t this line exist in NixOS (And why doesn’t it exist in the original RC file)?

I have no answer for the second qustion. The first one is interessting:

Did you notice the line /opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO and the others in the log above? We don’t really know, what brprintconflsr3 does. But it seems rather obvious it has something to do with our settings. Well, lets try to execute it manually:

/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# cannot open file !!

Mmh.

Ah, right, environment variables:

export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# Error: /tmp/HLL2375DW_latest_print_info :cannot open file !!

What?

Well, now the file is right, but why can the binary not open the file and change the settings?

Lets look at it:

-r--------  1 florian florian      183 Dez 14 15:03 HLL2375DW_latest_print_info

Why is my file 400? Lets change it to 666 (just to make sure) and try again:

export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
chmod 666 /tmp/HLL2375DW_latest_print_info
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# no output

Cool.

This is the culprit!

Btw: It is 400 because the original RCFILE is read-only, too. That kinda makes sense. It is used as reference and the script copies the reference (including the file permissions) and it therefore stays read-only.

The fix is rather easy and amounts to this diff:

--- a/opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper  2022-12-07 18:32:29.950543083 +0100
+++ b/opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper      2022-12-07 18:46:03.046989151 +0100
@@ -379,6 +379,7 @@


 `cp  $basedir/inf/br${PRINTER}rc  $TEMPRC`;
+`chmod 666  $TEMPRC`;
 $ENV{BRPRINTERRCFILE} = $TEMPRC;

 logprint( 0 , "TEMPRC    = $TEMPRC\n");

Now (after removing the DEBUG flag) the printer prints two-sided, in different resolutions and different paper sizes!

On ubuntu, the file original brHLL2375DWrc file gets linked to /etc/opt/brother/Printers/HLL2375DWE/inf and is read-write by root. Interesstingly the file is read-writable on NixOS, too. But for some reason gets the 400 permissions assigned in the /tmp directory. I haven’t figured out why. If someone knows, please let me know and I’ll update my post accordingly.

The updated PR can be found at https://github.com/NixOS/nixpkgs/pull/204306