diff --git a/xerox-acct/Makefile b/xerox-acct/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..c1d7bf32428d7f66c3ae657c72f7f18291800f5a
--- /dev/null
+++ b/xerox-acct/Makefile
@@ -0,0 +1,8 @@
+CC=gcc
+CFLAGS=-O2 -Wall -W -Wno-parentheses -Wstrict-prototypes -Wmissing-prototypes -Wundef -Wredundant-decls -std=gnu99
+
+all: xerox-acct
+
+clean:
+	rm -f `find . -name "*~" -or -name "*.[oa]" -or -name TAGS -or -name core -or -name .depend -or -name .#*`
+	rm -f xerox-acct
diff --git a/xerox-acct/xerox-acct.c b/xerox-acct/xerox-acct.c
new file mode 100644
index 0000000000000000000000000000000000000000..d24b1b3dfe34e285162fa499476e9a7b6ff9cc8b
--- /dev/null
+++ b/xerox-acct/xerox-acct.c
@@ -0,0 +1,211 @@
+/*
+ *	CUPS Filter Generating Xerox Accounting Attributes
+ *
+ *	(c) 2016 Martin Mares <mj@ucw.cz>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+
+#define PRINTF(i,j) __attribute__((format(printf,i,j)))
+#define NONRET __attribute__((noreturn))
+
+#if 0
+#define DEBUG(...) debug(__VA_ARGS__)
+#else
+#define DEBUG(...) do { } while(0)
+#endif
+
+static char *job_id;
+static char *job_user;
+static char *job_title;
+
+/*** Utility functions ***/
+
+static void PRINTF(1,2) NONRET bug(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+
+  fprintf(stderr, "ERROR: ACCT: ");
+  vfprintf(stderr, fmt, args);
+  fputc('\n', stderr);
+
+  va_end(args);
+  exit(1);
+}
+
+#define ASSERT(_cond) do { if (!(_cond)) bug("Assertion failed: %s", #_cond); } while (0)
+
+#if 0
+static void PRINTF(1,2) error(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+
+  fprintf(stderr, "ERROR: ACCT: ");
+  vfprintf(stderr, fmt, args);
+  fputc('\n', stderr);
+
+  va_end(args);
+}
+#endif
+
+#if 0
+static void PRINTF(1,2) debug(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+
+  fprintf(stderr, "DEBUG: ACCT: ");
+  vfprintf(stderr, fmt, args);
+  fputc('\n', stderr);
+
+  va_end(args);
+}
+#endif
+
+static char *xstrdup(const char *x)
+{
+  char *p = strdup(x);
+  if (!p)
+    bug("Out of memory");
+  return p;
+}
+
+/**
+ *  This is our parser of PJL. A hacky one, indeed, but it is expected
+ *  to parse only PJL directives generated by our PPD or by CUPS itself,
+ *  so we need not pay attention to all obscure details of the language.
+ **/
+
+#define LINESIZE 256
+
+static char *skip_spaces(char *p)
+{
+  while (*p == ' ' || *p == '\t')
+    p++;
+  return p;
+}
+
+static int my_toupper(int c)
+{
+  if (c >= 'a' && c <= 'z')
+    return c - 32;
+  else
+    return c;
+}
+
+static char *token_buf;
+
+static char *get_token(char **pp)
+{
+  char *token_start = token_buf;
+  char *tok = token_start;
+  char *p = skip_spaces(*pp);
+  if (!*p)
+    return NULL;
+  int c = my_toupper(*p);
+  if (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_')
+    {
+      while (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_')
+	{
+	  *tok++ = c;
+	  c = my_toupper(*++p);
+	}
+    }
+  else
+    *tok++ = *p++;
+  *tok++ = 0;
+  *pp = p;
+  token_buf = tok;
+  return token_start;
+}
+
+static void parse_pjl(void)
+{
+  static const char uel[] = "\e%-12345X";
+  for (int i=0; uel[i]; i++)
+    if (getchar() != uel[i])
+      bug("PJL error: No UEL found (pos %u)", i);
+  printf("\e%%-12345X");
+
+  char line[LINESIZE], tokens[2*LINESIZE];
+  for (;;)
+    {
+      int i = 0;
+      for (;;)
+	{
+	  int c = getchar();
+	  if (c < 0)
+	    bug("PJL error: Premature EOF");
+	  if (i < 4 && c != "@PJL"[i])
+	    bug("PJL error: Unrecognized line");
+	  if (c == '\r')
+	    continue;
+	  if (c == '\n')
+	      break;
+	  if (i >= LINESIZE-1)
+	    bug("PJL error: Line too long");
+	  line[i++] = c;
+	}
+      while (i > 0 && (line[i-1] == ' ' || line[i-1] == '\t'))
+	i--;
+      line[i] = 0;
+
+      DEBUG("PJL: %s", line);
+      char *p = line+1;
+      token_buf = tokens;
+      char *t, *t2;
+      if (!(t = get_token(&p)) || strcasecmp(t, "PJL"))
+	bug("PJL error: Malformed line");
+      if (!(t = get_token(&p)))
+	{
+	  puts(line);
+	  continue;
+	}
+
+      if (!strcasecmp(t, "ENTER"))
+	{
+	  if ((t2 = get_token(&p)) && !strcasecmp(t2, "LANGUAGE"))
+	    {
+	      puts(line);
+	      return;
+	    }
+	}
+
+      puts(line);
+    }
+}
+
+static void copy_body(void)
+{
+  char buf[1024];
+  size_t n;
+
+  while (n = fread(buf, 1, sizeof(buf), stdin))
+    {
+      if (fwrite(buf, 1, n, stdout) != n)
+	bug("Write error");
+    }
+}
+
+/*** Main ***/
+
+int main(int argc, char **argv)
+{
+  if (argc < 5)
+    {
+      fprintf(stderr, "Usage: xerox-acct <job> <user> <title> <num-copies> [<options>]\n");
+      return 1;
+    }
+  job_id = xstrdup(argv[1]);
+  job_user = xstrdup(argv[2]);
+  job_title = xstrdup(argv[3]);
+
+  parse_pjl();
+  copy_body();
+  return 0;
+}