diff --git a/includes/media.filter.inc b/includes/media.filter.inc
index 1936232..816e1db 100644
--- a/includes/media.filter.inc
+++ b/includes/media.filter.inc
@@ -313,12 +313,11 @@ function media_token_to_markup($match, $wysiwyg = FALSE) {
     $fields = media_filter_field_parser($tag_info);
 
     $attributes = is_array($tag_info['attributes']) ? $tag_info['attributes'] : array();
-    $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align'));
+    $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', _media_wysiwyg_allowed_attributes_default());
     $settings['attributes'] = array_intersect_key($attributes, array_flip($attribute_whitelist));
     $settings['fields'] = $fields;
 
     if (!empty($tag_info['attributes']) && is_array($tag_info['attributes'])) {
-      $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align'));
       $settings['attributes'] = array_intersect_key($tag_info['attributes'], array_flip($attribute_whitelist));
       $settings['fields'] = $fields;
 
@@ -668,18 +667,46 @@ function media_get_file_without_label($file, $view_mode, $settings = array()) {
   // support simple formatters that don't do this, set the element attributes to
   // what was requested, but not if the formatter applied its own logic for
   // element attributes.
-  if (!isset($element['#attributes']) && isset($settings['attributes'])) {
-    $element['#attributes'] = $settings['attributes'];
+  if (isset($settings['attributes'])) {
+    if (empty($element['#attributes'])) {
+      $element['#attributes'] = $settings['attributes'];
+    }
 
     // While this function may be called for any file type, images are a common
-    // use-case. theme_image() and theme_image_style() require the 'alt'
-    // attribute to be passed separately from the 'attributes' array (see
-    // http://drupal.org/node/999338). Until that's fixed, implement this
-    // special-case logic. Image formatters using other theme functions are
-    // responsible for their own 'alt' attribute handling. See
-    // theme_media_formatter_large_icon() for an example.
-    if (isset($settings['attributes']['alt']) && !isset($element['#alt']) && isset($element['#theme']) && in_array($element['#theme'], array('image', 'image_style'))) {
-      $element['#alt'] = $settings['attributes']['alt'];
+    // use-case, and image theme functions have their own structures for
+    // render arrays.
+    if (isset($element['#theme'])) {
+      switch ($element['#theme']) {
+        case 'image':
+        case 'image_style':
+          // theme_image() and theme_image_style() require the 'alt' attributes to
+          // be passed separately from the 'attributes' array. (see
+          // http://drupal.org/node/999338). Until that's fixed, implement this
+          // special-case logic. Image formatters using other theme functions are
+          // responsible for their own 'alt' attribute handling. See
+          // theme_media_formatter_large_icon() for an example.
+          if (empty($element['#alt']) && isset($settings['attributes']['alt'])) {
+            $element['#alt'] = $settings['attributes']['alt'];
+          }
+          break;
+
+        case 'image_formatter':
+          // theme_image_formatter() requires the attributes to be
+          // set on the item rather than the element itself.
+          if (empty($element['#item']['attributes'])) {
+            $element['#item']['attributes'] = $settings['attributes'];
+          }
+
+          // theme_image_formatter() also requires alt, title, height, and
+          // width attributes to be set on the item rather than within its
+          // attributes array.
+          foreach (array('alt', 'title', 'width', 'height') as $attr) {
+              if (isset($settings['attributes'][$attr])) {
+                  $element['#item'][$attr] = $settings['attributes'][$attr];
+              }
+          }
+          break;
+      }
     }
   }
 
diff --git a/js/media.filter.js b/js/media.filter.js
index c1b39c9..c0e3a89 100644
--- a/js/media.filter.js
+++ b/js/media.filter.js
@@ -14,13 +14,12 @@
      * @param content
      */
     replaceTokenWithPlaceholder: function(content) {
-      var tagmap = Drupal.media.filter.ensure_tagmap(),
+      Drupal.media.filter.ensure_tagmap()
+      var tagmap = Drupal.settings.tagmap,
         matches = content.match(/\[\[.*?\]\]/g),
-        media_definition,
-        id = 0;
+        media_definition;
 
       if (matches) {
-        var i = 1;
         for (var macro in tagmap) {
           var index = matches.indexOf(macro);
           if (index !== -1) {
@@ -38,10 +37,9 @@
               var element = Drupal.media.filter.create_element(tagmap[macro], media_definition);
               var markup = Drupal.media.filter.outerHTML(element);
 
-              content = content.replace(macro, Drupal.media.filter.getWrapperStart(i) + markup + Drupal.media.filter.getWrapperEnd(i));
+              content = content.replace(macro, markup);
             }
           }
-          i++;
         }
       }
       return content;
@@ -52,29 +50,23 @@
      * @param content
      */
     replacePlaceholderWithToken: function(content) {
-      var tagmap = Drupal.media.filter.ensure_tagmap();
-      var i = 1;
-      for (var macro in tagmap) {
-        var startTag = Drupal.media.filter.getWrapperStart(i), endTag = Drupal.media.filter.getWrapperEnd(i);
-        var startPos = content.indexOf(startTag), endPos = content.indexOf(endTag);
-        if (startPos !== -1 && endPos !== -1) {
-          // If the placeholder wrappers are empty, remove the macro too.
-          if (endPos - startPos - startTag.length === 0) {
-            macro = '';
-          }
-          content = content.substr(0, startPos) + macro + content.substr(endPos + (new String(endTag)).length);
-        }
-        i++;
-      }
-      return content;
-    },
-
-    getWrapperStart: function(i) {
-      return '<!--MEDIA-WRAPPER-START-' + i + '-->';
-    },
+      Drupal.media.filter.ensure_tagmap();
+      // Convert all xhtml markup to html for reliable matching/replacing.
+      content = content.replace(/[\s]\/\>/g, '>');
+
+      // Re-build the macros in case any element has changed in the editor.
+      $('.media-element', content).each(function(i, element) {
+        var markup = Drupal.media.filter.outerHTML($(element));
+          macro = Drupal.media.filter.create_macro($(element));
+
+        // Store the macro => html for more efficient rendering in
+        // replaceTokenWithPlaceholder().
+        Drupal.settings.tagmap[macro] = markup;
+        // Replace the media element with its macro.
+        content = content.replace(markup, macro);
+      });
 
-    getWrapperEnd: function(i) {
-      return '<!--MEDIA-WRAPPER-END-' + i + '-->';
+      return content;
     },
 
     /**
@@ -199,7 +191,7 @@
      * @param element (jQuery object)
      */
     outerHTML: function (element) {
-      return $('<div>').append(element.eq(0).clone()).html();
+      return element[0].outerHTML || $('<div>').append(element.eq(0).clone()).html();
     },
 
     /**
@@ -227,7 +219,7 @@
 
       // Return the wrapped html code to insert in an editor and use it with
       // replacePlaceholderWithToken()
-      return Drupal.media.filter.getWrapperStart(i) + markup + Drupal.media.filter.getWrapperEnd(i);
+      return markup;
     },
 
     /**
diff --git a/media.module b/media.module
index 7435518..61e7450 100644
--- a/media.module
+++ b/media.module
@@ -1068,6 +1068,23 @@ function media_file_displays_alter(&$displays, $file, $view_mode) {
         $file->{$field_name} = $value;}
     }
   }
+  // Alt and title are special.
+  // @see file_entity_file_load
+  $alt = variable_get('file_entity_alt', '[file:field_file_image_alt_text]');
+  $title = variable_get('file_entity_title', '[file:field_file_image_title_text]');
+
+  $replace_options = array(
+    'clear' => TRUE,
+    'sanitize' => FALSE,
+  );
+
+  // Load alt and title text from fields.
+  if (!empty($alt)) {
+    $file->alt = token_replace($alt, array('file' => $file), $replace_options);
+  }
+  if (!empty($title)) {
+    $file->title = token_replace($title, array('file' => $file), $replace_options);
+  }
 }
 
 /**
@@ -1300,3 +1317,27 @@ function _media_get_migratable_file_types() {
 
   return array_diff($types, $enabled_types);
 }
+
+/**
+ * Returns the default set of allowed attributes for use with WYSIWYG.
+ *
+ * @return Array of whitelisted attributes.
+ */
+function _media_wysiwyg_allowed_attributes_default() {
+  return array(
+    'alt',
+    'title',
+    'height',
+    'width',
+    'hspace',
+    'vspace',
+    'border',
+    'align',
+    'style',
+    'class',
+    'id',
+    'usemap',
+    'data-picture-group',
+    'data-picture-align',
+  );
+}
diff --git a/tests/media.file.usage.test b/tests/media.file.usage.test
index db5c614..39b2ea6 100644
--- a/tests/media.file.usage.test
+++ b/tests/media.file.usage.test
@@ -22,7 +22,7 @@ class MediaFileUsageTest extends MediaTestHelper {
    * Enable media and file entity modules for testing.
    */
   public function setUp() {
-    parent::setUp(array('media', 'file_entity'));
+    parent::setUp();
 
     // Create and log in a user.
     $account = $this->drupalCreateUser(array('administer nodes', 'create article content'));
@@ -30,72 +30,6 @@ class MediaFileUsageTest extends MediaTestHelper {
   }
 
   /**
-    * Generates markup to be inserted for a file.
-    *
-    * This is a PHP version of InsertMedia.insert() from js/wysiwyg-media.js.
-    *
-    * @param int $fid
-    *   Drupal file id
-    * @param int $count
-    *   Quantity of markup to insert
-    *
-    * @return string
-    *   Filter markup.
-    */
-  private function generateFileMarkup($fid, $count = 1) {
-    $file_usage_markup = '';
-
-    // Build the data that is used in a media tag.
-    $data = array(
-      'fid' => $fid,
-      'type' => 'media',
-      'view_mode' => 'preview',
-      'attributes' => array(
-        'height' => 100,
-        'width' => 100,
-        'classes' => 'media-element file_preview',
-      )
-    );
-
-    // Create the file usage markup.
-    for ($i = 1; $i <= $count; $i++) {
-      $file_usage_markup .= '<p>[[' . drupal_json_encode($data) . ']]</p>';
-    }
-
-    return $file_usage_markup;
-  }
-
-
-  /**
-   * Utility function to create a test node.
-   *
-   * @param int $fid
-   *   Create the node with media markup in the body field
-   *
-   * @return int
-   *   Returns the node id
-   */
-  private function createNode($fid = FALSE) {
-    $markup = '';
-    if (! empty($fid)) {
-      $markup = $this->generateFileMarkup($fid);
-    }
-
-    // Create an article node with file markup in the body field.
-    $edit = array(
-      'title' => $this->randomName(8),
-      'body[und][0][value]' => $markup,
-    );
-    // Save the article node. First argument is the URL, then the value array
-    // and the third is the label the button that should be "clicked".
-    $this->drupalPost('node/add/article', $edit, t('Save'));
-
-    // Get the article node that was saved by the unique title.
-    $node = $this->drupalGetNodeByTitle($edit['title']);
-    return $node->nid;
-  }
-
-  /**
    * Tests the tracking of file usages for files submitted via the WYSIWYG editor.
    */
   public function testFileUsageIncrementing() {
@@ -133,7 +67,7 @@ class MediaFileUsageTest extends MediaTestHelper {
     // Create a new revision that has two instances of the file. File usage will
     // be 4.
     $node = node_load($nid);
-    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 2);
+    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2);
     $node->revision = TRUE;
     node_save($node);
 
@@ -163,7 +97,7 @@ class MediaFileUsageTest extends MediaTestHelper {
 
     // Create a new revision that has the file on it. File usage will be 5.
     $node = node_load($nid);
-    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 1);
+    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1);
     $node->revision = TRUE;
     node_save($node);
 
@@ -194,7 +128,7 @@ class MediaFileUsageTest extends MediaTestHelper {
 
     // Create a new revision with the file on it twice. File usage will be 4.
     $node = node_load($nid);
-    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 2);
+    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2);
     $node->revision = TRUE;
     node_save($node);
 
@@ -206,7 +140,7 @@ class MediaFileUsageTest extends MediaTestHelper {
     // Re-save current revision with file on it once instead of twice. File
     // usage will be 3.
     $node = node_load($nid);
-    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 1);
+    $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1);
     $saved_vid = $node->vid;
     node_save($node);
 
diff --git a/tests/media.test b/tests/media.test
index aaf6dcd..3a7d92a 100644
--- a/tests/media.test
+++ b/tests/media.test
@@ -14,6 +14,171 @@ class MediaTestHelper extends DrupalWebTestCase {
    * Enable media and file entity modules for testing.
    */
   public function setUp() {
-    parent::setUp(array('media', 'file_entity'));
+    parent::setUp(array('token', 'media', 'file_entity'));
   }
+
+  /**
+    * Generates markup to be inserted for a file.
+    *
+    * This is a PHP version of InsertMedia.insert() from js/wysiwyg-media.js.
+    *
+    * @param int $fid
+    *   Drupal file id
+    * @param int $count
+    *   Quantity of markup to insert
+    * @param array $attributes
+    *   Extra attributes to insert.
+    * @param array $fields
+    *   Extra field values to insert.
+    *
+    * @return string
+    *   Filter markup.
+    */
+  protected function generateJsonTokenMarkup($fid, $count = 1, array $attributes = array(), array $fields = array()) {
+    $markup = '';
+    // Merge default atttributes.
+    $attributes += array(
+      'height' => 100,
+      'width' => 100,
+      'classes' => 'media-element file_preview',
+    );
+
+    // Build the data that is used in a media tag.
+    $data = array(
+      'fid' => $fid,
+      'type' => 'media',
+      'view_mode' => 'preview',
+      'attributes' => $attributes,
+      'fields' => $fields,
+    );
+
+    // Create the file usage markup.
+    for ($i = 1; $i <= $count; $i++) {
+      $markup .= '<p>[[' . drupal_json_encode($data) . ']]</p>';
+    }
+
+    return $markup;
+  }
+
+  /**
+   * Utility function to create a test node.
+   *
+   * @param int $fid
+   *   Create the node with media markup in the body field
+    * @param array $attributes
+    *   Extra attributes to insert to the file.
+    * @param array $fields
+    *   Extra field values to insert.
+   *
+   * @return int
+   *   Returns the node id
+   */
+  protected function createNode($fid = FALSE, array $attributes = array(), array $fields = array()) {
+    $markup = '';
+    if (! empty($fid)) {
+      $markup = $this->generateJsonTokenMarkup($fid, 1, $attributes, $fields);
+    }
+
+    // Create an article node with file markup in the body field.
+    $edit = array(
+      'title' => $this->randomName(8),
+      'body[und][0][value]' => $markup,
+    );
+    // Save the article node. First argument is the URL, then the value array
+    // and the third is the label the button that should be "clicked".
+    $this->drupalPost('node/add/article', $edit, t('Save'));
+
+    // Get the article node that was saved by the unique title.
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    return $node->nid;
+  }
+
+}
+
+class MediaWysiwygOverridesTest extends MediaTestHelper {
+
+  /**
+   * Provide test information.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => t('Media wysiwyg overrides'),
+      'description' => t('Tests that overriden attributes display correct.'),
+      'group' => t('Media'),
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    // Create and log in a user.
+    $account = $this->drupalCreateUser(array('administer nodes', 'create article content', 'administer filters', 'use text format filtered_html'));
+    $this->drupalLogin($account);
+
+    // Enable the media filter for full html.
+    $edit = array(
+      'filters[media_filter][status]' => TRUE,
+      'filters[filter_html][status]' => FALSE,
+    );
+    $this->drupalPost('admin/config/content/formats/filtered_html', $edit, t('Save configuration'));
+  }
+
+  /**
+    * Test image media overrides.
+    */
+  public function testAttributeOverrides() {
+    $files = $this->drupalGetTestFiles('image');
+    $file = file_save($files[0]);
+
+    // Create a node to test with.
+    $nid = $this->createNode($file->fid);
+
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw('height="100" width="100"', t('Image displays with default attributes.'));
+
+    // Create a node with overriden attributes.
+    $attributes = array(
+      'style' => 'float: left; width: 50px;',
+    );
+    $nid = $this->createNode($file->fid, $attributes);
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw('style="float: left; width: 50px;"', t('Image displays with inline attributes.'));
+
+    // Create a node with overriden alt/title.
+    $attributes = array(
+      'alt' => $this->randomName(),
+      'title' => $this->randomName(),
+    );
+    $nid = $this->createNode($file->fid, $attributes);
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes.'));
+
+    // Create a node with overriden alt/title fields.
+    $fields = $attributes = array();
+    $attributes['alt'] = $fields['field_file_image_alt_text[und][0][value]'] = $this->randomName();
+    $attributes['title'] = $fields['field_file_image_title_text[und][0][value]'] = $this->randomName();
+
+    $this->drupalGet('file/' . $file->fid . '/edit');
+    $this->drupalGet('file/' . $file->fid);
+    $this->drupalGet('admin/config/media/file-settings');
+    $nid = $this->createNode($file->fid, array(), $fields);
+    $this->drupalGet('node/' . $nid);
+    // Ensure that the alt/title from attributes display rather the field ones.
+    $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as fields.'));
+
+    // Create a node with overriden alt/title fields as well as attributes.
+    $attributes = array(
+      'alt' => $this->randomName(),
+      'title' => $this->randomName(),
+    );
+    $fields = array(
+      'field_file_image_alt_text[und][0][value]' => $this->randomName(),
+      'field_file_image_title_text[und][0][value]' => $this->randomName(),
+    );
+    $nid = $this->createNode($file->fid, $attributes, $fields);
+    $this->drupalGet('node/' . $nid);
+    // Ensure that the alt/title from attributes display rather the field ones.
+    $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes overriding field values.'));
+  }
+
 }
diff --git a/wysiwyg_plugins/media.inc b/wysiwyg_plugins/media.inc
index d90b79f..611ee60 100644
--- a/wysiwyg_plugins/media.inc
+++ b/wysiwyg_plugins/media.inc
@@ -68,7 +68,7 @@ function media_include_browser_js() {
     }
   }
   // Add wysiwyg-specific settings.
-  $settings = array('wysiwyg_allowed_attributes' => variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align')));
+  $settings = array('wysiwyg_allowed_attributes' => variable_get('media__wysiwyg_allowed_attributes', _media_wysiwyg_allowed_attributes_default()));
   drupal_add_js(array('media' => $settings), 'setting');
 }
 
