File: src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java
Issue: Preserve doesn't preserve whitespace at start of line
What steps will reproduce the problem?

Code such as:
/**
 * @preserve

This
  was
    ASCII
       Art

*/

What is the expected output? What do you see instead?

The words line up on the left:
/*
This
was
ASCII
Art
*/

What version of the product are you using? On what operating system?

Live web verison.

Please provide any additional information below.

Buggy code:
private ExtractionInfo extractMultilineTextualBlock(JsDocToken token,
                                                    WhitespaceOption option) {

  if (token == JsDocToken.EOC || token == JsDocToken.EOL ||
      token == JsDocToken.EOF) {
    return new ExtractionInfo("", token);
  }

  stream.update();
  int startLineno = stream.getLineno();
  int startCharno = stream.getCharno() + 1;

  // Read the content from the first line.
  String line = stream.getRemainingJSDocLine();
  if (option != WhitespaceOption.PRESERVE) {
    line = line.trim();
  }

  StringBuilder builder = new StringBuilder();
  builder.append(line);

  state = State.SEARCHING_ANNOTATION;
  token = next();

  boolean ignoreStar = false;

  // Track the start of the line to count whitespace that
  // the tokenizer skipped. Because this case is rare, it's easier
  // to do this here than in the tokenizer.

  do {
    switch (token) {
      case STAR:
        if (ignoreStar) {
          // Mark the position after the star as the new start of the line.
        } else {
          // The star is part of the comment.
          if (builder.length() > 0) {
            builder.append(' ');
          }

          builder.append('*');
        }

        token = next();
        continue;

      case EOL:
        if (option != WhitespaceOption.SINGLE_LINE) {
          builder.append("\n");
        }

        ignoreStar = true;
        token = next();
        continue;

      default:
        ignoreStar = false;
        state = State.SEARCHING_ANNOTATION;

            // All tokens must be separated by a space.

        if (token == JsDocToken.EOC ||
            token == JsDocToken.EOF ||
            // When we're capturing a license block, annotations
            // in the block are ok.
            (token == JsDocToken.ANNOTATION &&
             option != WhitespaceOption.PRESERVE)) {
          String multilineText = builder.toString();

          if (option != WhitespaceOption.PRESERVE) {
            multilineText = multilineText.trim();
          }

          int endLineno = stream.getLineno();
          int endCharno = stream.getCharno();

          if (multilineText.length() > 0) {
            jsdocBuilder.markText(multilineText, startLineno, startCharno,
                endLineno, endCharno);
          }

          return new ExtractionInfo(multilineText, token);
        }

        if (builder.length() > 0) {
          builder.append(' ');
        }
        builder.append(toString(token));

        line = stream.getRemainingJSDocLine();

        if (option != WhitespaceOption.PRESERVE) {
          line = trimEnd(line);
        }

        builder.append(line);
        token = next();
    }
  } while (true);
}

Fixed code:
private ExtractionInfo extractMultilineTextualBlock(JsDocToken token,
                                                    WhitespaceOption option) {

  if (token == JsDocToken.EOC || token == JsDocToken.EOL ||
      token == JsDocToken.EOF) {
    return new ExtractionInfo("", token);
  }

  stream.update();
  int startLineno = stream.getLineno();
  int startCharno = stream.getCharno() + 1;

  // Read the content from the first line.
  String line = stream.getRemainingJSDocLine();
  if (option != WhitespaceOption.PRESERVE) {
    line = line.trim();
  }

  StringBuilder builder = new StringBuilder();
  builder.append(line);

  state = State.SEARCHING_ANNOTATION;
  token = next();

  boolean ignoreStar = false;

  // Track the start of the line to count whitespace that
  // the tokenizer skipped. Because this case is rare, it's easier
  // to do this here than in the tokenizer.
  int lineStartChar = -1;

  do {
    switch (token) {
      case STAR:
        if (ignoreStar) {
          // Mark the position after the star as the new start of the line.
          lineStartChar = stream.getCharno() + 1;
        } else {
          // The star is part of the comment.
          if (builder.length() > 0) {
            builder.append(' ');
          }

          builder.append('*');
        }

        token = next();
        continue;

      case EOL:
        if (option != WhitespaceOption.SINGLE_LINE) {
          builder.append("\n");
        }

        ignoreStar = true;
        lineStartChar = 0;
        token = next();
        continue;

      default:
        ignoreStar = false;
        state = State.SEARCHING_ANNOTATION;

        boolean isEOC = token == JsDocToken.EOC;
        if (!isEOC) {
          if (lineStartChar != -1 && option == WhitespaceOption.PRESERVE) {
            int numSpaces = stream.getCharno() - lineStartChar;
            for (int i = 0; i < numSpaces; i++) {
              builder.append(' ');
            }
            lineStartChar = -1;
          } else if (builder.length() > 0) {
            // All tokens must be separated by a space.
            builder.append(' ');
          }
        }

        if (token == JsDocToken.EOC ||
            token == JsDocToken.EOF ||
            // When we're capturing a license block, annotations
            // in the block are ok.
            (token == JsDocToken.ANNOTATION &&
             option != WhitespaceOption.PRESERVE)) {
          String multilineText = builder.toString();

          if (option != WhitespaceOption.PRESERVE) {
            multilineText = multilineText.trim();
          }

          int endLineno = stream.getLineno();
          int endCharno = stream.getCharno();

          if (multilineText.length() > 0) {
            jsdocBuilder.markText(multilineText, startLineno, startCharno,
                endLineno, endCharno);
          }

          return new ExtractionInfo(multilineText, token);
        }

        builder.append(toString(token));

        line = stream.getRemainingJSDocLine();

        if (option != WhitespaceOption.PRESERVE) {
          line = trimEnd(line);
        }

        builder.append(line);
        token = next();
    }
  } while (true);
}