diff --git a/xcpt.c b/xcpt.c
index deca8dd9021e9a9c12cc66a7fa1ded157c485c5d..4dac72577ddfe8420ae9b3e0b6bb3d8be9e0a073 100644
--- a/xcpt.c
+++ b/xcpt.c
@@ -33,6 +33,8 @@ static void PRINTF(1,2) NONRET bug(const char *fmt, ...)
   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, ...)
 {
@@ -101,11 +103,11 @@ static void set_option(char *key, char *val)
   for (int i=0; i<OPT_MAX; i++)
     if (!strcmp(opt_names[i], key))
       {
-	debug("Setting %s=<%s>\n", key, val);
+	debug("Setting %s=<%s>", key, val);
 	opt[i] = xstrdup(val);
 	return;
       }
-  debug("Unrecognized option <%s>\n", key);
+  debug("Unrecognized option <%s>", key);
 }
 
 /**
@@ -231,6 +233,231 @@ static void parse_pjl(void)
     }
 }
 
+/*** XCPT generator ***/
+
+#define STACK_DEPTH 10
+
+static int xcpt_level;
+static const char *xcpt_stack[STACK_DEPTH];
+
+static void xcpt_start_line(void)
+{
+  printf("@PJL XCPT ");
+  for (int i=0; i<xcpt_level; i++)
+    putchar('\t');
+}
+
+static void xcpt_printf(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+
+  xcpt_start_line();
+  vprintf(fmt, args);
+  putchar('\n');
+
+  va_end(args);
+}
+
+static void xcpt_push(const char *name)
+{
+  ASSERT(xcpt_level < STACK_DEPTH);
+  xcpt_stack[xcpt_level++] = xstrdup(name);
+}
+
+static void xcpt_open(const char *name)
+{
+  xcpt_printf("<%s>", name);
+  xcpt_push(name);
+}
+
+static void xcpt_close(void)
+{
+  ASSERT(xcpt_level);
+  const char *name = xcpt_stack[--xcpt_level];
+  xcpt_printf("</%s>", name);
+  free((char *) name);
+}
+
+static void xcpt_integer(const char *name, int value)
+{
+  xcpt_printf("<%s syntax=\"integer\">%d</%s>", name, value, name);
+}
+
+static void xcpt_keyword(const char *name, const char *value)
+{
+  xcpt_printf("<%s syntax=\"keyword\">%s</%s>", name, value, name);
+}
+
+static void xcpt_enum(const char *name, int value)
+{
+  xcpt_printf("<%s syntax=\"enum\">%d</%s>", name, value, name);
+}
+
+static void xcpt_media_type(const char *name, const char *value)
+{
+  xcpt_printf("<%s syntax=\"mimeMediaType\">%s</%s>", name, value, name);
+}
+
+static void xcpt_collection_open(const char *name)
+{
+  xcpt_printf("<%s syntax=\"collection\">", name);
+  xcpt_push(name);
+}
+
+static void xcpt_set_open(const char *name)
+{
+  xcpt_printf("<%s syntax=\"1setOf\">", name);
+  xcpt_push(name);
+}
+
+static void xcpt_cdata(const char *text)
+{
+  int c;
+  while (c = *text++)
+    switch (c)
+      {
+      case '<':
+	printf("&lt;");
+	break;
+      case '>':
+	printf("&gt;");
+	break;
+      case '&':
+	printf("&amp;");
+	break;
+      case '"':
+	printf("&quot;");
+	break;
+      default:
+	putchar(c);
+      }
+}
+
+static void xcpt_text(const char *name, const char *text)
+{
+  xcpt_start_line();
+  printf("<%s syntax=\"text\" xml:space=\"preserve\">", name);
+  xcpt_cdata(text);
+  printf("</%s>\n", name);
+}
+
+static void xcpt_name(const char *name, const char *text)
+{
+  xcpt_start_line();
+  printf("<%s syntax=\"name\" xml:space=\"preserve\">", name);
+  xcpt_cdata(text);
+  printf("</%s>\n", name);
+}
+
+static void gen_job_template(void)
+{
+  xcpt_integer("adjust-contrast", 0);
+  xcpt_integer("adjust-cyan-red", 0);
+  xcpt_integer("adjust-lightness", 0);
+  xcpt_integer("adjust-magenta-green", 0);
+  xcpt_integer("adjust-saturation", 0);
+  xcpt_integer("adjust-yellow-blue", 0);
+  xcpt_keyword("color-adjustment-set", "automatic");
+  xcpt_keyword("color-effects-type", "color");
+  xcpt_integer("copies", 1);
+  xcpt_keyword("document-reading-orientation", "portrait");
+  xcpt_keyword("embedded-profiles", "automatic");
+
+  xcpt_set_open("finishings");
+  xcpt_enum("value", 3);
+  xcpt_close();
+
+  xcpt_keyword("halftone-text", "ignore-pdl");
+  xcpt_keyword("hold-for-authentication", "none");
+
+  xcpt_collection_open("interleaved-sheets-col");
+  xcpt_keyword("interleaved-sheets-type", "none");
+  xcpt_close();
+
+  xcpt_set_open("job-offset");
+  xcpt_keyword("value", "offset-set");
+  xcpt_close();
+
+  xcpt_collection_open("job-save-disposition");
+  xcpt_keyword("save", "none");
+  xcpt_close();
+
+  xcpt_keyword("job-sheets", "job-start-sheet");
+
+  xcpt_collection_open("client-default-attributes-col");
+    xcpt_collection_open("media-col");
+    xcpt_keyword("input-tray", "automatic");
+    xcpt_keyword("media-color", "white");
+    xcpt_collection_open("media-size");
+      xcpt_integer("x-dimension", 21000);
+      xcpt_integer("y-dimension", 29700);
+      xcpt_close();
+    xcpt_keyword("media-type", "system-default");
+    xcpt_close();
+  xcpt_keyword("print-quality-level", "standard");
+  xcpt_keyword("sides", "two-sided-long-edge");
+  xcpt_close();
+
+  xcpt_keyword("output-bin", "automatic");
+  xcpt_keyword("print-settings", "none");
+  xcpt_keyword("rendering-intent-graphics", "automatic");
+  xcpt_keyword("rendering-intent-images", "automatic");
+  xcpt_keyword("rendering-intent-text", "automatic");
+  xcpt_keyword("sheet-collate", "collated");
+  xcpt_keyword("spot-color-mapping", "use-local-printer-values");
+  xcpt_keyword("undefined-source-cmyk-images", "automatic");
+  xcpt_keyword("undefined-source-cmyk-text", "automatic");
+  xcpt_keyword("undefined-source-gray-images", "automatic");
+  xcpt_keyword("undefined-source-gray-text", "automatic");
+  xcpt_keyword("undefined-source-rgb-images", "automatic");
+  xcpt_keyword("undefined-source-rgb-text", "automatic");
+  xcpt_keyword("undefined-spot-color-images", "automatic");
+  xcpt_keyword("undefined-spot-color-text", "automatic");
+}
+
+static void gen_job_operation(void)
+{
+  xcpt_keyword("creator-name-attributes", "windows-ps-driver");
+  xcpt_keyword("creator-name-pdl", "explorer");
+  xcpt_text("creator-version-attributes", "cups-xcpt");
+  xcpt_text("creator-version-pdl", "6.1.7601.17567");	// FIXME
+  xcpt_media_type("document-format", "application/postscript");		// FIXME
+  xcpt_name("job-id-from-client", "TOP SECRET JOB #1");
+  xcpt_name("job-name", "Test Page");
+  xcpt_name("job-originating-user-domain", "KAM");
+  xcpt_name("job-originating-user-name", "nobody");
+  xcpt_name("requesting-user-name", "nobody");
+}
+
+static void gen_xcpt(void)
+{
+  printf("\e%%-12345X@PJL JOB\n");
+  xcpt_printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+  xcpt_printf("<!DOCTYPE xpif SYSTEM \"xpif-v02081.dtd\">");
+  xcpt_printf("<xpif version=\"1.0\" cpss-version=\"2.07\" xml:lang=\"en-US\">");
+  xcpt_push("xpif");
+
+  xcpt_open("job-template-attributes");
+  gen_job_template();
+  xcpt_close();
+
+  xcpt_open("job-operation-attributes");
+  gen_job_operation();
+  xcpt_close();
+
+  xcpt_close();
+  ASSERT(!xcpt_level);
+  printf("@PJL ENTER LANGUAGE = %s\n", opt[OPT_LANGUAGE]);
+}
+
+static void copy_body(void)
+{
+  // FIXME
+}
+
+/*** Main ***/
+
 int main(int argc, char **argv)
 {
   if (argc < 5)
@@ -246,7 +473,9 @@ int main(int argc, char **argv)
   parse_pjl();
 
   for (int i=0; i<OPT_MAX; i++)
-    DEBUG("Option %s=<%s>\n", opt_names[i], opt[i]);
+    DEBUG("Option %s=<%s>", opt_names[i], opt[i]);
 
+  gen_xcpt();
+  copy_body();
   return 0;
 }