public ScrambleRequest(String title, String scrambleRequestUrl, String seed)
      throws InvalidScrambleRequestException, UnsupportedEncodingException {
    String[] puzzle_count_copies_scheme = scrambleRequestUrl.split("\\*");
    title = URLDecoder.decode(title, "utf-8");
    for (int i = 0; i < puzzle_count_copies_scheme.length; i++) {
      puzzle_count_copies_scheme[i] = URLDecoder.decode(puzzle_count_copies_scheme[i], "utf-8");
    }
    String countStr = "";
    String copiesStr = "";
    String scheme = "";
    String puzzle;
    switch (puzzle_count_copies_scheme.length) {
      case 4:
        scheme = puzzle_count_copies_scheme[3];
      case 3:
        copiesStr = puzzle_count_copies_scheme[2];
      case 2:
        countStr = puzzle_count_copies_scheme[1];
      case 1:
        puzzle = puzzle_count_copies_scheme[0];
        break;
      default:
        throw new InvalidScrambleRequestException("Invalid puzzle request " + scrambleRequestUrl);
    }

    LazyInstantiator<Puzzle> lazyScrambler = puzzles.get(puzzle);
    if (lazyScrambler == null) {
      throw new InvalidScrambleRequestException("Invalid scrambler: " + puzzle);
    }

    try {
      this.scrambler = lazyScrambler.cachedInstance();
    } catch (Exception e) {
      throw new InvalidScrambleRequestException(e);
    }

    ScrambleCacher scrambleCacher = scrambleCachers.get(puzzle);
    if (scrambleCacher == null) {
      scrambleCacher = new ScrambleCacher(scrambler);
      scrambleCachers.put(puzzle, scrambleCacher);
    }

    this.title = title;
    fmc = countStr.equals("fmc");
    int count;
    if (fmc) {
      count = 1;
    } else {
      count = Math.min(toInt(countStr, 1), MAX_COUNT);
    }
    this.copies = Math.min(toInt(copiesStr, 1), MAX_COPIES);
    if (seed != null) {
      this.scrambles = scrambler.generateSeededScrambles(seed, count);
    } else {
      this.scrambles = scrambleCacher.newScrambles(count);
    }

    this.colorScheme = scrambler.parseColorScheme(scheme);
  }
  private static TableAndHighlighting createTable(
      PdfWriter docWriter,
      Document doc,
      float sideMargins,
      Dimension scrambleImageSize,
      String[] scrambles,
      Puzzle scrambler,
      HashMap<String, Color> colorScheme,
      String scrambleNumberPrefix,
      boolean forceHighlighting)
      throws DocumentException {
    PdfContentByte cb = docWriter.getDirectContent();

    PdfPTable table = new PdfPTable(3);

    int charsWide = scrambleNumberPrefix.length() + 1 + (int) Math.log10(scrambles.length);
    String wideString = "";
    for (int i = 0; i < charsWide; i++) {
      // M has got to be as wide or wider than the widest digit in our font
      wideString += "M";
    }
    wideString += ".";
    float col1Width = new Chunk(wideString).getWidthPoint();
    // I don't know why we need this, perhaps there's some padding?
    col1Width += 5;

    float availableWidth = doc.getPageSize().getWidth() - sideMargins;
    float scrambleColumnWidth =
        availableWidth - col1Width - scrambleImageSize.width - 2 * SCRAMBLE_IMAGE_PADDING;
    int availableScrambleHeight = scrambleImageSize.height - 2 * SCRAMBLE_IMAGE_PADDING;

    table.setTotalWidth(
        new float[] {
          col1Width, scrambleColumnWidth, scrambleImageSize.width + 2 * SCRAMBLE_IMAGE_PADDING
        });
    table.setLockedWidth(true);

    String longestScramble = "";
    String longestPaddedScramble = "";
    for (String scramble : scrambles) {
      if (scramble.length() > longestScramble.length()) {
        longestScramble = scramble;
      }

      String paddedScramble = padTurnsUniformly(scramble, "M");
      if (paddedScramble.length() > longestPaddedScramble.length()) {
        longestPaddedScramble = paddedScramble;
      }
    }
    // I don't know how to configure ColumnText.fitText's word wrapping characters,
    // so instead, I just replace each character I don't want to wrap with M, which
    // should be the widest character (we're using a monospaced font,
    // so that doesn't really matter), and won't get wrapped.
    char widestCharacter = 'M';
    longestPaddedScramble = longestPaddedScramble.replaceAll("\\S", widestCharacter + "");
    boolean tryToFitOnOneLine = true;
    if (longestPaddedScramble.indexOf("\n") >= 0) {
      // If the scramble contains newlines, then we *only* allow wrapping at the
      // newlines.
      longestPaddedScramble = longestPaddedScramble.replaceAll(" ", "M");
      tryToFitOnOneLine = false;
    }
    boolean oneLine = false;
    Font scrambleFont = null;

    try {
      BaseFont courier = BaseFont.createFont(BaseFont.COURIER, BaseFont.CP1252, BaseFont.EMBEDDED);
      Rectangle availableArea =
          new Rectangle(
              scrambleColumnWidth - 2 * SCRAMBLE_PADDING_HORIZONTAL,
              availableScrambleHeight
                  - SCRAMBLE_PADDING_VERTICAL_TOP
                  - SCRAMBLE_PADDING_VERTICAL_BOTTOM);
      float perfectFontSize =
          fitText(
              new Font(courier),
              longestPaddedScramble,
              availableArea,
              MAX_SCRAMBLE_FONT_SIZE,
              true);
      if (tryToFitOnOneLine) {
        String longestScrambleOneLine = longestScramble.replaceAll(".", widestCharacter + "");
        float perfectFontSizeForOneLine =
            fitText(
                new Font(courier),
                longestScrambleOneLine,
                availableArea,
                MAX_SCRAMBLE_FONT_SIZE,
                false);
        oneLine = perfectFontSizeForOneLine >= MINIMUM_ONE_LINE_FONT_SIZE;
        if (oneLine) {
          perfectFontSize = perfectFontSizeForOneLine;
        }
      }
      scrambleFont = new Font(courier, perfectFontSize, Font.NORMAL);
    } catch (IOException e) {
      l.log(Level.INFO, "", e);
    } catch (DocumentException e) {
      l.log(Level.INFO, "", e);
    }

    boolean highlight = forceHighlighting;
    for (int i = 0; i < scrambles.length; i++) {
      String scramble = scrambles[i];
      String paddedScramble =
          oneLine ? scramble : padTurnsUniformly(scramble, NON_BREAKING_SPACE + "");
      Chunk ch = new Chunk(scrambleNumberPrefix + (i + 1) + ".");
      PdfPCell nthscramble = new PdfPCell(new Paragraph(ch));
      nthscramble.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
      table.addCell(nthscramble);

      Phrase scramblePhrase = new Phrase();
      int nthLine = 1;
      LinkedList<Chunk> lineChunks =
          splitScrambleToLineChunks(paddedScramble, scrambleFont, scrambleColumnWidth);
      if (lineChunks.size() >= MIN_LINES_TO_ALTERNATE_HIGHLIGHTING) {
        highlight = true;
      }

      for (Chunk lineChunk : lineChunks) {
        if (highlight && (nthLine % 2 == 0)) {
          lineChunk.setBackground(HIGHLIGHT_COLOR);
        }
        scramblePhrase.add(lineChunk);
        nthLine++;
      }

      PdfPCell scrambleCell = new PdfPCell(new Paragraph(scramblePhrase));
      // We carefully inserted newlines ourselves to make stuff fit, don't
      // let itextpdf wrap lines for us.
      scrambleCell.setNoWrap(true);
      scrambleCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
      // This shifts everything up a little bit, because I don't like how
      // ALIGN_MIDDLE works.
      scrambleCell.setPaddingTop(-SCRAMBLE_PADDING_VERTICAL_TOP);
      scrambleCell.setPaddingBottom(SCRAMBLE_PADDING_VERTICAL_BOTTOM);
      scrambleCell.setPaddingLeft(SCRAMBLE_PADDING_HORIZONTAL);
      scrambleCell.setPaddingRight(SCRAMBLE_PADDING_HORIZONTAL);
      // We space lines a little bit more here - it still fits in the cell height
      scrambleCell.setLeading(0, 1.1f);
      table.addCell(scrambleCell);

      if (scrambleImageSize.width > 0 && scrambleImageSize.height > 0) {
        PdfTemplate tp =
            cb.createTemplate(
                scrambleImageSize.width + 2 * SCRAMBLE_IMAGE_PADDING,
                scrambleImageSize.height + 2 * SCRAMBLE_IMAGE_PADDING);
        Graphics2D g2 =
            new PdfGraphics2D(tp, tp.getWidth(), tp.getHeight(), new DefaultFontMapper());
        g2.translate(SCRAMBLE_IMAGE_PADDING, SCRAMBLE_IMAGE_PADDING);

        try {
          Svg svg = scrambler.drawScramble(scramble, colorScheme);
          drawSvgToGraphics2D(svg, g2, scrambleImageSize);
        } catch (Exception e) {
          table.addCell("Error drawing scramble: " + e.getMessage());
          l.log(
              Level.WARNING,
              "Error drawing scramble, if you're having font issues, try installing ttf-dejavu.",
              e);
          continue;
        } finally {
          g2.dispose(); // iTextPdf blows up if we do not dispose of this
        }
        PdfPCell imgCell = new PdfPCell(Image.getInstance(tp), true);
        imgCell.setBackgroundColor(BaseColor.LIGHT_GRAY);
        imgCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        imgCell.setHorizontalAlignment(PdfPCell.ALIGN_MIDDLE);
        table.addCell(imgCell);
      } else {
        table.addCell("");
      }
    }

    TableAndHighlighting tableAndHighlighting = new TableAndHighlighting();
    tableAndHighlighting.table = table;
    tableAndHighlighting.highlighting = highlight;
    return tableAndHighlighting;
  }