@Override
  public void produceVisibleOutput(QContent qc, boolean bInit, boolean bPlain) throws OmException {
    // Get actual current value of string
    String sCurrent = getQuestion().applyPlaceholders(sEquation);

    if (bPlain) {
      // Put text equivalent
      Element eDiv =
          qc.createElement("div"); // Can't use span because they aren't allowed to contain things
      eDiv.setAttribute("style", "display:inline");
      qc.addInlineXHTML(eDiv);
      XML.createText(eDiv, (bSpaceBefore ? " " : "") + getString("alt") + (bSpaceAfter ? " " : ""));
      qc.addTextEquivalent(getString("alt"));

      // Put each placeholder
      for (int i = 0; i < apPlaces.length; i++) {
        // Check content is not hidden
        Place p = apPlaces[i];
        if ((!p.bImplicit && !p.qc.isDisplayed()) || (p.bImplicit && !p.qc.isChildDisplayed()))
          continue;

        // Label followed by content of placeholder
        Element ePlace = XML.createChild(eDiv, "div");
        if (p.sLabel != null) {
          Element eLabel = XML.createChild(ePlace, p.sLabelFor == null ? "span" : "label");
          XML.createText(eLabel, p.sLabel + " ");
          qc.addTextEquivalent(p.sLabel);
          if (p.sLabelFor != null)
            eLabel.setAttribute(
                "for", LabelComponent.getLabel(getQDocument(), bPlain, p.sLabelFor));
        }
        qc.setParent(ePlace);
        p.qc.produceOutput(qc, bInit, bPlain);
        qc.unsetParent();
      }
    } else {
      // Check background colour, foreground, and zoom
      Color cBackground = getBackground();
      if (cBackground == null) cBackground = Color.white;
      Color cForeground =
          getQuestion().isFixedColour()
              ? convertRGB(getQuestion().getFixedColourFG())
              : Color.black;
      double dZoom = getQuestion().getZoom();

      // Hash to filename/identifier
      String sFilename =
          "eq"
              + (sCurrent.hashCode()
                  + cBackground.hashCode() * 3
                  + cForeground.hashCode() * 7
                  + (new Double(dZoom)).hashCode() * 11)
              + (getBoolean(PROPERTY_TEXTFONT) ? "t" : "e")
              + ".png";

      // Make actual image if needed, also get placeholder positions
      if (!sFilename.equals(sSent)) {
        e = Equation.create(sCurrent, (float) dZoom);
        if (getBoolean(PROPERTY_TEXTFONT)) e.setFont("Verdana", new int[] {13, 11, 9});
        BufferedImage bi = e.render(cForeground, cBackground, true);
        qc.addResource(sFilename, "image/png", QContent.convertPNG(bi));
        for (int i = 0; i < apPlaces.length; i++) {
          Point p = e.getPlaceholder(apPlaces[i].sID);
          apPlaces[i].iActualX = p.x;
          apPlaces[i].iActualY = p.y;
        }
        sSent = sFilename;
      }

      Element eEnsureSpaces = qc.createElement("div");
      eEnsureSpaces.setAttribute("class", "equation");
      qc.addInlineXHTML(eEnsureSpaces);

      // If there's a space before, add one here too (otherwise IE eats it)
      if (bSpaceBefore) XML.createText(eEnsureSpaces, " ");

      String sImageID = QDocument.ID_PREFIX + getID() + "_img";
      Element eImg = XML.createChild(eEnsureSpaces, "img");
      eImg.setAttribute("id", sImageID);
      eImg.setAttribute("onmousedown", "return false;"); // Prevent Firefox drag/drop
      eImg.setAttribute("src", "%%RESOURCES%%/" + sFilename);
      eImg.setAttribute("alt", getString("alt"));
      eImg.setAttribute("style", "vertical-align:-" + (e.getHeight() - e.getBaseline()) + "px;");

      if (bSpaceAfter) XML.createText(eEnsureSpaces, " ");

      qc.addTextEquivalent(getString("alt"));

      String sJavascript = "addOnLoad(function() { inlinePositionFix('" + sImageID + "'";

      for (int i = 0; i < apPlaces.length; i++) {
        Place p = apPlaces[i];

        // Must get the label even though not using it, just to indicate that
        // it's been used
        if (p.sLabelFor != null) LabelComponent.getLabel(getQDocument(), bPlain, p.sLabelFor);

        int iEffectiveWidth = (int) Math.round(dZoom * p.iWidth),
            iEffectiveHeight = (int) Math.round(dZoom * p.iHeight);

        String sPlaceholderID = QDocument.ID_PREFIX + getID() + "_" + p.sID;
        Element ePlace = XML.createChild(eEnsureSpaces, "div");
        ePlace.setAttribute("class", "placeholder");
        ePlace.setAttribute("id", sPlaceholderID);
        ePlace.setAttribute(
            "style",
            "width:"
                + iEffectiveWidth
                + "px; "
                + "height:"
                + iEffectiveHeight
                + "px; "
                + "visibility:hidden;");

        QComponent qcPlaceComponent = p.qc;

        if (qcPlaceComponent.isPropertyDefined(PROPERTY_FORCEWIDTH)
            && qcPlaceComponent.isPropertyDefined(PROPERTY_FORCEHEIGHT)) {
          qcPlaceComponent.setInteger(PROPERTY_FORCEWIDTH, iEffectiveWidth);
          qcPlaceComponent.setInteger(PROPERTY_FORCEHEIGHT, iEffectiveHeight);
        }

        if (p.sLabel != null) qc.addTextEquivalent(p.sLabel);
        qc.setParent(ePlace);
        qcPlaceComponent.produceOutput(qc, bInit, bPlain);
        qc.unsetParent();

        sJavascript += ",['" + sPlaceholderID + "'," + p.iActualX + "," + p.iActualY + "]";
      }
      sJavascript += "); });";

      if (apPlaces.length > 0) // No JS needed if there weren't any placeholders
      {
        Element eScript = XML.createChild(eEnsureSpaces, "script");
        eScript.setAttribute("type", "text/javascript");
        XML.createText(eScript, sJavascript);
      }
    }
  }