Обработка проблем с изменением масштаба изображения (округления) в 4.1 (WP Ticket # 18532)

17

В настоящее время я выполняю миграцию содержимого сайта со старого сайта до 4.1 на новую настройку и решаю проблему с ошибкой округления # 18532 и соответствующим исправлением .

Подводя итог, можно сказать, что исправлено давнее поведение при округлении на стороне WordPress:

Представьте, что мы загружаем изображение размером 693х173 и масштабируем его до ширины 300:

  • до 4.1: 300х74
  • пост 4.1: 300х75

Проблема

Как правило, это не вызывает никаких проблем, потому что существующие файлы <img>не затрагиваются.

Но когда вы регенерируете большие пальцы или импортируете вложения из файла WXR, они генерируются по-разному в файловой системе, оставляя все <img>в post_contentживых.

Ищем решение

Я думал о различных решениях:

Возвращаясь к плохим старым временам

Changeset 30660 представил новый фильтр, wp_constrain_dimensionsкоторый можно использовать, чтобы просто подключить старое поведение, предшествующее 4.1, обратно. Это решает проблему. Но мне интересно, может ли это вызвать проблемы позже, и, как правило, я бы хотел исправить это, поэтому, хотя это работает, я бы посчитал его неидеальным.

Времена, когда они меняются

Таким образом, это ставит нас перед другой целью: очистить БД и заменить все ссылки на старые файлы ссылками на новые файлы. Вопрос, который я сейчас задаю, заключается в том, как это сделать. Я ищу эффективное и общеприменимое решение, так как подозреваю, что эта проблема затрагивает многих людей.

Моя текущая идея заключается в следующем:

  1. Импортируйте, регенерируйте или что угодно, что оставляет нас с новыми файлами и битыми тегами.
  2. Создайте список A из всех файлов с измененным размером в файловой системе или, альтернативно, получите эту информацию из базы данных.
  3. Проанализируйте этот список и создайте второй список B с именами файлов, смещенными на один пиксель, как это было бы до 4.1
  4. Выполните поиск и замените всю базу данных, заменив все вхождения B соответствующей записью в A

Я просто не уверен, что это самый умный и эффективный способ справиться с этой ситуацией. Он также чувствует себя слишком грубо. Поэтому, прежде чем приступить к реализации, я просто хотел проверить бесконечную мудрость толпы WPSE;)

[править] Прочитав ответ ck-macleod (спасибо!), я думаю, что исправление должно решить это раз и навсегда, поэтому вам не нужно постоянно держать эту проблему в затылке. [/редактировать]

[edit2] Я только что нашел связанный билет на Trac . Добавление для справки. [/ edit2]

kraftner
источник
где ты имел error issue of #13852ввиду #18532? :)
Аравона
2
Ой, да, исправлено. :)
Крафтнер

Ответы:

4

Это другой подход, отличный от другого ответа, который работает при импорте контента с помощью импортера и исправляет URL-адреса раз и навсегда. Опять же: это не проверено в бою, но это решение, на котором я остановился, и оно сработало.

Я предпочитаю это, поскольку это решает проблему раз и навсегда, и если это работает, это работает. Так как вы не оставляете испорченные вещи в БД и исправляете их на дисплее, вам не нужно беспокоиться о том, что что-то сломается позже.

/*
Plugin Name:  Bugfix Ticket 31581 for WP_Importer
Plugin URI:   /wordpress//a/206992/47733
Description:  Fixes image references after post WP 4.1 image scaling change in post content when using the WP_Importer  (see https://core.trac.wordpress.org/ticket/31581)
Version:      0.0.1
*/

class Bugfix31581WPImporter {

    protected $remaps;

    /**
     * Initialize class, mainly setting up hooks
     */
    public function init(){

        if (WP_IMPORTING === true){

            $this->remaps = array();

            //This hook is chosen because it is pretty close to where the actual attachment import is happening.
            //TODO: May be reconsidered.
            add_filter('wp_update_attachment_metadata', array($this, 'collectRemaps'), 10 , 2);

            add_action('import_end', array($this, 'remap'));
            add_action('import_end', array($this, 'importEnded'));
        }

    }

    /**
     * Cleans up hooks after the import has ended.
     */
    public function importEnded(){
        remove_filter('wp_update_attachment_metadata', array($this, 'collectRemaps'), 10);

        remove_action('import_end', array($this, 'remap'), 10);
        remove_action('import_end', array($this, 'importEnded'), 10);
    }

    /**
     * When an attachment is added compare the resulting sizes with the sizes from the legacy algorithm and setup remap.
     *
     * @param $data
     * @param $post_id
     *
     * @return mixed
     */
    public function collectRemaps($data, $post_id ){

        $intermediate_sizes = $this->getIntermediateSizes();

        if(empty($data) || !array_key_exists('sizes', $data)){
            return $data;
        }

        foreach($data['sizes'] as $key => $size){

            $size_new_algorithm = array($size['width'], $size['height']);

            $dest_w = $intermediate_sizes[$key]['width'];
            $dest_h = $intermediate_sizes[$key]['height'];
            $crop = $intermediate_sizes[$key]['crop'];

            add_filter('wp_constrain_dimensions', array($this, 'legacy_wp_constrain_dimensions'), 10, 5);

            $size_old_algorithm = image_resize_dimensions($data['width'], $data['height'], $dest_w, $dest_h, $crop);

            //Bail out in the rare case of `image_resize_dimensions` returning false
            if($size_old_algorithm===false){
                continue;
            }

            $size_old_algorithm = array($size_old_algorithm[4], $size_old_algorithm[5]);

            remove_filter('wp_constrain_dimensions', array($this, 'legacy_wp_constrain_dimensions'), 10);

            // Compare the current size with the calculation of the old algorithm...
            $diff = array_diff($size_new_algorithm, $size_old_algorithm);

            // ...to detect any mismatches
            if(!empty($diff)){

                $oldFilename = $this->getOldFilename($size['file'], $size_old_algorithm);

                // If getting the old filename didn't work for some reason (probably other filename-structure) bail out.
                if($oldFilename===false){
                    continue;
                }

                if(!array_key_exists($post_id, $this->remaps)){
                    $this->remaps[$post_id] = array();
                }

                $this->remaps[$post_id][$size['file']] = array(
                    'file' => $oldFilename,
                    'size' => $key
                );
            }

        }

        return $data;
    }

    /**
     * Get resize settings for all image sizes
     *
     * Taken from wp_generate_attachment_metadata() in includes/image.php
     *
     * @return array
     */
    public function getIntermediateSizes(){

        global $_wp_additional_image_sizes;

        $sizes = array();
        foreach ( get_intermediate_image_sizes() as $s ) {
            $sizes[$s] = array( 'width' => '', 'height' => '', 'crop' => false );
            if ( isset( $_wp_additional_image_sizes[$s]['width'] ) )
                $sizes[$s]['width'] = intval( $_wp_additional_image_sizes[$s]['width'] ); // For theme-added sizes
            else
                $sizes[$s]['width'] = get_option( "{$s}_size_w" ); // For default sizes set in options
            if ( isset( $_wp_additional_image_sizes[$s]['height'] ) )
                $sizes[$s]['height'] = intval( $_wp_additional_image_sizes[$s]['height'] ); // For theme-added sizes
            else
                $sizes[$s]['height'] = get_option( "{$s}_size_h" ); // For default sizes set in options
            if ( isset( $_wp_additional_image_sizes[$s]['crop'] ) )
                $sizes[$s]['crop'] = $_wp_additional_image_sizes[$s]['crop']; // For theme-added sizes
            else
                $sizes[$s]['crop'] = get_option( "{$s}_crop" ); // For default sizes set in options
        }

        return $sizes;
    }

    /**
     * Turn the new filename into the old filename reducing the height by one
     *
     * @param $newFilename
     * @param $size
     *
     * @return mixed
     */
    public function getOldFilename($newFilename, $size){

        $dimensions = array();

        $filetypes = $this->getAllowedImageExtentions();

        // TODO: This pattern can be different. See `image_make_intermediate_size` in image editor implementation.
        $matchFiles = '/([0-9]{1,5})x([0-9]{1,5}).(' . $filetypes . ')$/';

        // Extract the dimensions
        preg_match($matchFiles,$newFilename,$dimensions);

        // If the file URL doesn't allow guessing the dimensions bail out.
        if(empty($dimensions)){
            return $newFilename;
        }

        $newStub = $dimensions[1] . 'x' . $dimensions[2] . '.' . $dimensions[3];

        $oldStub = $size[0] . 'x' . $size[1] . '.' . $dimensions[3];

        $oldFilename = str_replace($newStub,$oldStub,$newFilename);

        return $oldFilename;
    }

    /**
     * Extract all file extensions that match an image/* mime type
     *
     * @return string
     */
    protected function getAllowedImageExtentions(){
        $allowed_filetypes = get_allowed_mime_types();

        $allowed_images = array();

        foreach($allowed_filetypes as $extensions => $mimetype){
            if( substr($mimetype,0,6) == 'image/' ){
                $allowed_images[] = $extensions;
            }
        }

        return implode('|',$allowed_images);
    }


    /**
     * This is the heart of this class. Based on the collected remaps from earlier it does a S&R on the DB.
     */
    public function remap(){

        global $wpdb;

        foreach($this->remaps as $attachment_id => $replaces){

            foreach($replaces as $new_url => $old_data){

                $to_url = wp_get_attachment_image_src($attachment_id,$old_data['size']);
                $to_url = $to_url[0];

                $from_url = str_replace($new_url, $old_data['file'], $to_url);

                // remap urls in post_content
                $wpdb->query( $wpdb->prepare("UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url) );

                //TODO: This is disabled as enclosures can't be images, right?
                // remap enclosure urls
                //$result = $wpdb->query( $wpdb->prepare("UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url) );

            }

        }

    }

    /**
     * This is a copy of the legacy pre 4.1 wp_constrain_dimensions()
     *
     * @param $dimensions
     * @param $current_width
     * @param $current_height
     * @param $max_width
     * @param $max_height
     *
     * @return array
     */
    public function legacy_wp_constrain_dimensions($dimensions, $current_width, $current_height, $max_width, $max_height){
        if ( !$max_width and !$max_height )
            return array( $current_width, $current_height );

        $width_ratio = $height_ratio = 1.0;
        $did_width = $did_height = false;

        if ( $max_width > 0 && $current_width > 0 && $current_width > $max_width ) {
            $width_ratio = $max_width / $current_width;
            $did_width = true;
        }

        if ( $max_height > 0 && $current_height > 0 && $current_height > $max_height ) {
            $height_ratio = $max_height / $current_height;
            $did_height = true;
        }

        // Calculate the larger/smaller ratios
        $smaller_ratio = min( $width_ratio, $height_ratio );
        $larger_ratio  = max( $width_ratio, $height_ratio );

        if ( intval( $current_width * $larger_ratio ) > $max_width || intval( $current_height * $larger_ratio ) > $max_height )
            // The larger ratio is too big. It would result in an overflow.
            $ratio = $smaller_ratio;
        else
            // The larger ratio fits, and is likely to be a more "snug" fit.
            $ratio = $larger_ratio;

        // Very small dimensions may result in 0, 1 should be the minimum.
        $w = max ( 1, intval( $current_width  * $ratio ) );
        $h = max ( 1, intval( $current_height * $ratio ) );

        // Sometimes, due to rounding, we'll end up with a result like this: 465x700 in a 177x177 box is 117x176... a pixel short
        // We also have issues with recursive calls resulting in an ever-changing result. Constraining to the result of a constraint should yield the original result.
        // Thus we look for dimensions that are one pixel shy of the max value and bump them up
        if ( $did_width && $w == $max_width - 1 )
            $w = $max_width; // Round it up
        if ( $did_height && $h == $max_height - 1 )
            $h = $max_height; // Round it up

        return array( $w, $h );
    }

}

add_filter('import_start',array(new Bugfix31581WPImporter(),'init'));
kraftner
источник
Хорошая работа +1.
Gmazzap
1

Решение проблемы глобально и идеально для ВСЕХ файлов изображений (и ссылок) на большом сайте - учитывая, например, возможность, что люди могут иногда переименовывать файлы изображений вручную, имитируя стиль WP - и другие странные варианты - может быть затруднительно. Операции по поиску и замене в базе данных также связаны с осложнениями (и рисками!).

Могли бы вы справиться с подавляющим большинством ошибок - неработающими изображениями и неработающими ссылками на изображения, я полагаю, - и достичь желаемого конечного результата или приемлемого факсимильного сообщения следующим способом?

  1. Определите дату, до которой все изображения с измененным размером были изменены с помощью старого метода «intval», а не нового «круглого» метода. (Можно также создать другой вид отсечения, но дата кажется более простой.)

  2. Для всех опубликованных сообщений <= дата завершения, запустите preg_replace для the_content () во время загрузки / рендеринга, захватив все файлы изображений с проблемным шаблоном или шаблонами и заменив их требуемым шаблоном. База данных останется неизменной, но вывод будет безошибочным в большинстве случаев. Я не уверен, что решение должно будет применяться как к «единственному» содержимому публикации страниц, так и к архивированию страниц и других процессов.

Если бы решение такого типа было бы полезным, то следующий вопрос состоял бы в том, могут ли модели проблемы и замены быть адекватно определены. Из вашего списка предлагаемых решений видно, что, возможно, несколько типичных шаблонов могут быть фактически изолированы (возможно, взяты из предыдущих настроек мультимедиа, создающих миниатюры и некоторые другие изображения).

Я уже написал более простую функцию, которую я использую (и нахожусь в процессе превращения в плагин), которая глобально заменяет все файлы изображений в назначенных каталогах, до определенной даты, изображением по умолчанию или ссылкой на изображение, согласно вышеописанному способу. Это было для сайта, где, из-за чрезмерной осторожности с авторскими правами, операторы просто удалили все свои изображения, не подозревая, что, помимо получения ужасных результатов на старых страницах, они также выдают тысячи ошибок, по две на каждую образ.

Если вы можете сузить образец проблемы более конкретно, и случаи, когда выходные данные должны были бы быть изменены, тогда я мог бы видеть о подключении его к моему формату - который не очень сложен, и который для лучшего RegExer, чем я мог бы даже быть легким. С другой стороны, я не хотел бы тратить ваше или мое время, если этот подход не решит проблему для вас.

CK MacLeod
источник
Спасибо за ваш взгляд на это! Несколько мыслей: я думаю, что иметь неправильные данные в БД и просто вносить исправления на экране - не очень чистое и устойчивое решение. Это может привести к поломке в любое время и ухудшить производительность в каждом представлении. Это также может иметь непредсказуемые побочные эффекты, например, для других плагинов, которые анализируют или изменяют содержимое другим способом. В зависимости от того, как это сделано, изображения все еще ломаются в бэкэнде. В этом случае я думаю, что просто сбросить масштабирование с помощью, wp_constrain_dimensionsкак упоминалось в вопросе, при выполнении импорта и воздержаться от восстановления превью было бы чище.
Крафтнер
Не за что. Дело в том, что данные в БД - это не неправильные данные, это просто те данные, которые вам больше не нужны в новом режиме. Что касается снижения производительности, я думаю, что оно, вероятно, будет минимальным, особенно потому, что оно теоретически применимо только к сообщениям до даты X. В более общем смысле, может не быть единого лучшего решения: я думаю, что хорошее Достаточное решение может варьироваться в зависимости от характера сайта, прошлых приложений и привычек обработки изображений, размера БД, практических и временных ограничений и так далее.
CK MacLeod
Вы, вероятно, правы, что не будет единого решения для всех. В настоящее время я изучаю различные способы решения этой проблемы, среди которых подход на рендеринге, похожий на ваш, и подход на импорт, который я бы предпочел, поскольку он решает это раз и навсегда. Посмотрим, к чему это приведет. :)
Крафтнер
1

Хорошо, это базовый подход к замене сломанных изображений на лету. Имейте в виду, что это скорее подтверждение концепции, чем проверенное в бою решение. Он просто подключает the_contentфильтр, который может (возможно, имеет) некоторые нежелательные побочные эффекты в некоторых ситуациях. Обращаться осторожно. :)

Хотя в коде тоже так сказано, я также хочу отметить @Rarst за этот ответ, использованный в моем коде.

/*
Plugin Name:  Bugfix 31581 Live
Plugin URI:   /wordpress//a/206986/47733
Description:  Fixes image references in post content after post 4.1 image scaling change (see https://core.trac.wordpress.org/ticket/31581)
Version:      0.0.1
*/

class Bugfix31581Live {

    protected $matchURLs;
    protected $matchFiles;

    protected $remaps;

    public function init(){

        $filetypes = $this->get_allowed_image_extentions();

        $baseurl = wp_upload_dir();
        $baseurl = preg_quote($baseurl['baseurl'], '/');

        $this->matchURLs = '/' . $baseurl . '\/.??([a-zA-Z0-9_-]*?\.(?:' . $filetypes . '))/';

        //TODO: This pattern can be different. See `image_make_intermediate_size` in image editor implementation
        $this->matchFiles = '/([0-9]{1,4})x([0-9]{1,4}).(' . $filetypes . ')$/';

        add_filter('the_content', array($this, 'update_urls') );
    }

    public function update_urls($content){

        $urls = array();

        preg_match_all($this->matchURLs,$content,$urls);

        // Bail out early if we don't have any match.
        if($urls === false || empty($urls[0])){
            return $content;
        }

        // Loop through all matches
        foreach($urls[0] as $url){

            // Try to resolve this URL to an attachment ID
            $id = $this->get_attachment_id($url);

            // If not  let's see if this might be a URL that has been broken by our beloved Changeset 30660
            if( $id === false ){

                $dimensions = array();

                // Extract the dimensions
                preg_match($this->matchFiles,$url,$dimensions);

                // If the file URL doesn't allow guessing the dimensions bail out.
                if(empty($dimensions)){
                    continue;
                }

                // Old filename
                $old = $dimensions[1] . 'x' . $dimensions[2] . '.' . $dimensions[3];

                // New filename (not sure if this exists yet)
                $new = $dimensions[1] . 'x' . ($dimensions[2]+1) . '.' . $dimensions[3];

                // Build the new URL (not sure if this exists yet)
                $new_url = str_replace($old,$new,$url);

                // Try to get the attachment with the new url
                $id = $this->get_attachment_id($new_url);

                // Bad luck. This also doesn't exist.
                if( $id === false ) {
                    continue;
                }

                // Just to be sure everything is in sync we get the URL built from id and size.
                $db_url = wp_get_attachment_image_src($id,array($dimensions[1], $dimensions[2]+1));

                // Check if the URL we created and the one wp_get_attachment_image_src builds are the same.
                if($new_url === $db_url[0]){

                    // Awesome let's replace the broken URL.
                    $content = str_replace($url,$new_url,$content);
                }

            }

        }

        return $content;
    }

    /**
     * Get the Attachment ID for a given image URL.
     *
     * @link   /wordpress//a/7094
     *
     * @param  string $url
     *
     * @return boolean|integer
     */
    protected function get_attachment_id( $url ) {

        $dir = wp_upload_dir();

        // baseurl never has a trailing slash
        if ( false === strpos( $url, $dir['baseurl'] . '/' ) ) {
            // URL points to a place outside of upload directory
            return false;
        }

        $file  = basename( $url );
        $query = array(
            'post_type'  => 'attachment',
            'fields'     => 'ids',
            'meta_query' => array(
                array(
                    'value'   => $file,
                    'compare' => 'LIKE',
                ),
            )
        );

        $query['meta_query'][0]['key'] = '_wp_attached_file';

        // query attachments
        $ids = get_posts( $query );

        if ( ! empty( $ids ) ) {

            foreach ( $ids as $id ) {

                $tmp = wp_get_attachment_image_src( $id, 'full' );

                // first entry of returned array is the URL
                if ( $url === array_shift( $tmp ) )
                    return $id;
            }
        }

        $query['meta_query'][0]['key'] = '_wp_attachment_metadata';

        // query attachments again
        $ids = get_posts( $query );

        if ( empty( $ids) )
            return false;

        foreach ( $ids as $id ) {

            $meta = wp_get_attachment_metadata( $id );

            foreach ( $meta['sizes'] as $size => $values ) {

                $tmp = wp_get_attachment_image_src( $id, $size );

                if ( $values['file'] === $file && $url === array_shift( $tmp ) )
                    return $id;
            }
        }

        return false;
    }

    protected function get_allowed_image_extentions(){
        $allowed_filetypes = get_allowed_mime_types();

        $allowed_images = array();

        foreach($allowed_filetypes as $extensions => $mimetype){
            if( substr($mimetype,0,6) == 'image/' ){
                $allowed_images[] = $extensions;
            }
        }

        return implode('|',$allowed_images);
    }

}

add_filter('init',array(new Bugfix31581Live(),'init'));
kraftner
источник