Gérer les problèmes de changement de mise à l'échelle de l'image (arrondi) dans 4.1 (ticket WP # 18532)

17

Je suis actuellement en train de migrer le contenu d'un site d'un ancien site pré 4.1 vers une nouvelle configuration et de rencontrer un problème avec le problème d'erreur d'arrondi # 18532 et le correctif correspondant .

Pour résumer cela a corrigé un mauvais comportement d'arrondi de longue date du côté de WordPress:

Imaginez que nous téléchargions une image avec 693x173 et la redimensionnions à une largeur de 300:

  • avant 4.1: 300x74
  • poste 4.1: 300x75

Le problème

Généralement, cela ne pose aucun problème car les fichiers existants ne <img>sont pas touchés.

Mais lors de la régénération vous pouces ou des pièces jointes importation d'un fichier WXR ils sont générés différemment dans le système de fichiers laissant tous <img>dans post_contentmorts.

Vous cherchez une solution

J'ai pensé à différentes solutions:

Revenir au mauvais vieux temps

Changeset 30660 a introduit un nouveau filtre wp_constrain_dimensionsqui peut être utilisé pour simplement réintégrer l'ancien comportement d'avant 4.1. Cela résout le problème. Mais je me demande si cela pourrait causer des problèmes plus tard et en général, j'aimerais avoir le correctif, même si cela fonctionne, je le jugerais non idéal.

Les temps sont en train de changer'

Cela nous laisse donc un autre objectif: nettoyer la base de données et remplacer toutes les références aux anciens fichiers par des références aux nouveaux fichiers. La question que je pose actuellement ici est de savoir comment procéder. Je recherche une solution efficace et d'application générale car je soupçonne que ce problème affecte et affectera beaucoup de gens

Mon idée actuelle est la suivante:

  1. Importez, régénérez ou tout ce qui nous laisse avec les nouveaux fichiers et les balises cassées.
  2. Créer une liste A à partir de tous les fichiers redimensionnés du système de fichiers ou obtenir ces informations de la base de données
  3. Analyser cette liste et créer une deuxième liste B avec des noms de fichiers tous décalés d'un pixel comme ils le feraient avant 4.1
  4. Effectuez une recherche et remplacez l'ensemble de la base de données en remplaçant toutes les occurrences de B par l'entrée correspondante dans A

Je ne sais tout simplement pas si c'est la façon la plus intelligente et la plus efficace de gérer cette situation. Cela semble également un peu trop brutal. Donc, avant de le mettre en œuvre, je voulais juste vérifier avec la sagesse infinie de la foule WPSE;)

[edit] Après avoir lu la réponse de ck-macleod (merci!) je pense qu'un correctif devrait résoudre ce problème une fois pour toutes afin que vous n'ayez pas besoin de garder constamment ce problème à l'arrière de votre tête. [/Éditer]

[edit2] Je viens de trouver un ticket associé sur Trac . Ajout pour référence. [/ edit2]

kraftner
la source
où avez-vous voulu error issue of #13852dire #18532? :)
Aravona
2
Oups, oui, fixe. :)
kraftner

Réponses:

4

Il s'agit d'une autre approche que l'autre réponse qui fonctionne lors de l'importation de contenu avec l'importateur et corrige les URL une fois pour toutes. Encore une fois: ce n'est pas testé au combat, mais c'est la solution sur laquelle j'ai opté et cela a fonctionné.

Je préfère cela car cela résout le problème une fois pour toutes et si cela fonctionne, cela fonctionne. Comme vous ne laissez pas de trucs cassés dans la base de données et les réparez à l'écran, vous n'avez pas à vous soucier des trucs qui se brisent plus tard.

/*
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
la source
Beau travail un +1.
gmazzap
1

Résoudre le problème globalement et parfaitement pour TOUS les fichiers d'image (et liens) dans un grand site - étant donné, par exemple, que des individus peuvent avoir parfois renommé des fichiers d'image en imitant manuellement le style WP - et d'autres variations étranges - pourraient être difficiles. Les opérations de recherche et de remplacement de bases de données impliquent également des complications (et des risques!).

Pourriez-vous gérer la grande majorité des erreurs - images cassées et liens d'images cassés, je présume - et obtenir le résultat final souhaité ou un fac-similé raisonnable, par la méthode suivante?

  1. Identifiez la date avant laquelle toutes les images redimensionnées ont été redimensionnées par l'ancienne méthode "intval" plutôt que par la nouvelle méthode "round". (Un autre type de coupure pourrait également être créé, mais la date semble plus simple.)

  2. Pour tous les articles publiés <= la date limite, exécutez preg_replace sur the_content () au moment du chargement / rendu, en capturant tous les fichiers image avec le ou les motifs de problème et en les remplaçant par le motif souhaité. La base de données resterait inchangée, mais la sortie serait sans erreur dans la plupart des cas. Je ne sais pas si la solution devrait s'appliquer à la fois au contenu de publication de page "singulier" et aux pages d'archivage et autres processus également.

Si une solution de ce type était utile, alors la question suivante serait de savoir si les modèles de problème et les remplacements pourraient être définis de manière adéquate. Il semble à partir de votre liste de solutions proposées que peut-être quelques schémas typiques pourraient en fait être isolés (peut-être tirés de paramètres de supports antérieurs produisant des miniatures et d'autres images).

J'ai déjà écrit une fonction plus simple que j'utilise (et je suis en train de devenir un plug-in), qui remplace globalement tous les fichiers image dans les répertoires désignés, jusqu'à une certaine date, avec une image ou un lien d'image par défaut, selon la méthode décrite ci-dessus. C'était pour un site où, par excès de prudence en matière de droit d'auteur, les opérateurs ont simplement supprimé toutes leurs images, ignorant qu'en plus de produire des résultats moches sur les anciennes pages, ils produisaient également des milliers d'erreurs, deux pour chaque image.

Si vous pouvez affiner le modèle de problème plus spécifiquement et les cas où la sortie devrait être modifiée, alors je pourrais voir comment le brancher dans mon format - ce qui n'est pas très compliqué, et qui pour un meilleur RegExer que je pourrais même être facile. D'un autre côté, je ne voudrais pas perdre votre temps ou mon temps si cette approche ne répond pas au problème pour vous.

CK MacLeod
la source
Merci pour votre avis! Quelques réflexions: je pense que le fait d'avoir des données erronées dans la base de données et de simplement les patcher à l'écran n'est pas une solution très propre et durable. Cela peut casser à tout moment et nuire aux performances de chaque vue. Cela peut également avoir des effets secondaires imprévisibles, par exemple pour d'autres plugins qui analysent ou modifient le contenu d'une autre manière. Selon la façon dont cela se fait, les images sont toujours cassées dans le backend. Dans ce cas, je pense que simplement réinitialiser la mise à l'échelle via wp_constrain_dimensionscomme mentionné dans la question tout en faisant l'importation et s'abstenir de reconstruire les pouces serait plus propre.
kraftner
De rien. Le fait est que les données de la base de données ne sont pas des données erronées, ce ne sont tout simplement plus les données que vous souhaitez sous le nouveau régime. Sur le plan des performances, je pense que ce serait probablement minime, d'autant plus qu'il ne s'applique, en théorie, qu'aux publications avant la date X. Plus généralement, il n'y a peut-être pas de meilleure solution universelle: je pense que le bon une solution suffisante peut varier selon le caractère du site, les anciennes applications et habitudes de gestion des images, la taille de la base de données, les contraintes pratiques et temporelles, etc.
CK MacLeod,
Vous avez probablement raison de dire qu'il n'y aura pas de solution universelle. J'explore actuellement différentes façons de gérer cela, parmi lesquelles une approche sur rendu similaire à la vôtre et une approche sur import que je préfère car elle résout cela une fois pour toutes. Nous verrons où cela mène. :)
kraftner
1

D'accord, c'est une approche de base pour remplacer les images cassées à la volée. Sachez qu'il s'agit davantage d'une preuve de concept que d'une solution éprouvée au combat. Il accroche simplement le the_contentfiltre, ce qui pourrait (probablement) avoir des effets secondaires indésirables dans certaines situations. Manipuler avec soin. :)

Bien qu'il le dise aussi dans le code, je veux aussi créditer @Rarst pour cette réponse utilisée dans mon code.

/*
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
la source