Obsługa problemów ze zmianą skalowania obrazu (zaokrąglanie) w 4.1 (WP Ticket # 18532)

17

Obecnie jestem w trakcie migracji zawartości witryny ze starej witryny w wersji wcześniejszej niż 4.1 do nowej konfiguracji i trafiam na problem z błędem zaokrąglenia # 18532 i odpowiednią poprawką .

Podsumowując, naprawiono długotrwałe złe zaokrąglanie po stronie WordPressa:

Wyobraźmy sobie, że przesyłamy obraz o wymiarach 693 x 173 i skalujemy do szerokości 300:

  • przed 4.1: 300 x 74
  • post 4.1: 300 x 75

Problem

Zasadniczo nie powoduje to żadnych problemów, ponieważ istniejące pliki <img>nie są dotykane.

Ale kiedy regenerujący kciuki lub importowanie załączników z pliku WXR dostają różnie generowane w systemie plików pozostawiając wszystko <img>w post_contentmartwy.

Poszukuję rozwiązania

Myślałem o różnych rozwiązaniach:

Wracając do złych, starych czasów

Changeset 30660 wprowadził nowy filtr, wp_constrain_dimensionsktórego można użyć do podłączenia starego zachowania sprzed wersji 4.1. To rozwiązuje problem. Ale zastanawiam się, czy może to spowodować później problemy i ogólnie chciałbym to naprawić, więc chociaż działa, uważam, że nie jest idealny.

Czasy się zmieniają'

Pozostaje nam zatem kolejny cel: oczyścić bazę danych i zastąpić wszystkie odniesienia do starych plików odniesieniami do nowych plików. Pytanie, które tutaj zadaję, brzmi: jak to zrobić. Szukam skutecznego i ogólnie stosowalnego rozwiązania, ponieważ podejrzewam, że ten problem dotyczy i wpłynie na wiele osób

Mój obecny pomysł jest następujący:

  1. Importuj, regeneruj lub cokolwiek, co pozostawia nam nowe pliki i uszkodzone tagi.
  2. Utwórz listę A ze wszystkich plików o zmienionym rozmiarze w systemie plików lub uzyskaj te informacje z bazy danych
  3. Przeanalizuj tę listę i utwórz drugą listę B z nazwami plików przesuniętymi o jeden piksel, tak jak wyglądałby przed 4.1
  4. Wykonaj wyszukiwanie i zamień w całej bazie danych, zastępując wszystkie wystąpienia B odpowiednim wpisem w A

Po prostu nie jestem pewien, czy jest to najinteligentniejszy i najskuteczniejszy sposób poradzenia sobie z tą sytuacją. Czuje się też trochę zbyt brutalnie. Przed wdrożeniem chciałem tylko sprawdzić z nieskończoną mądrością tłumu WPSE;)

[edytuj] Po przeczytaniu odpowiedzi ck-macleod (dzięki!) Myślę, że poprawka powinna rozwiązać to raz na zawsze, więc nie musisz ciągle trzymać tego problemu z tyłu głowy. [/edytować]

[edit2] Właśnie znalazłem powiązany bilet na Trac . Dodawanie w celach informacyjnych. [/ edit2]

Kraftner
źródło
gdzie miałeś error issue of #13852na myśli #18532? :)
Aravona
2
Ups, tak, naprawione. :)
kraftner

Odpowiedzi:

4

Jest to inne podejście niż inna odpowiedź, która działa podczas importowania treści za pomocą importera i naprawia adresy URL raz na zawsze. Znowu: To nie jest testowany w bitwie, ale rozwiązanie, na którym się zdecydowałem i zadziałało.

Wolę to, ponieważ rozwiązuje problem raz na zawsze, a jeśli działa, działa. Ponieważ nie zostawiasz uszkodzonych elementów w bazie danych i nie naprawiasz ich na ekranie, nie musisz martwić się późniejszym uszkodzeniem elementów.

/*
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
źródło
Dobra robota +1.
gmazzap
1

Rozwiązanie problemu globalnie i doskonale dla WSZYSTKICH plików obrazów (i linków) w dużej witrynie - biorąc pod uwagę, na przykład, możliwość, że niektóre osoby czasami zmieniają nazwy plików obrazów ręcznie imitując styl WP - i inne dziwne odmiany - może być trudne. Operacje wyszukiwania i zamiany bazy danych pociągają za sobą również komplikacje (i ryzyko!).

Czy potrafisz poradzić sobie z ogromną większością błędów - zepsutymi obrazami i zepsutymi linkami do obrazów - i osiągnąć pożądany efekt końcowy lub rozsądny faks za pomocą następującej metody?

  1. Określ datę, przed którą wszystkie obrazy o zmienionym rozmiarze zostały przeskalowane przy użyciu starej metody „intval” zamiast nowej metody „okrągłej”. (Można również stworzyć inny rodzaj granicy, ale data wydaje się najłatwiejsza).

  2. Dla wszystkich opublikowanych postów <= data graniczna, uruchom preg_replace na the_content () w czasie ładowania / renderowania, przechwytując wszystkie pliki obrazów z problematycznym wzorem lub wzorami i zastępując je pożądanym wzorem. Baza danych pozostanie niezmieniona, ale w większości przypadków dane wyjściowe będą wolne od błędów. Nie jestem pewien, czy rozwiązanie musiałoby dotyczyć zarówno „pojedynczej” zawartości postów stron, jak i archiwizować strony i inne procesy.

Jeśli rozwiązanie tego typu byłoby pomocne, następnym pytaniem byłoby, czy wzorce problemów i zamienniki mogłyby być odpowiednio zdefiniowane. Z listy proponowanych rozwiązań wynika, że ​​prawdopodobnie można wyodrębnić kilka typowych wzorców (być może zaczerpniętych z wcześniejszych ustawień multimediów tworzących miniatury i inne obrazy).

Napisałem już prostszą funkcję, której używam (i jestem w trakcie przekształcania się we wtyczkę), która globalnie zastępuje wszystkie pliki obrazów w wyznaczonych katalogach, do określonej daty, domyślnym obrazem lub linkiem do obrazu, zgodnie z wyżej opisaną metodą. Była to witryna, w której operatorzy usunęli wszystkie swoje zdjęcia w nadmiarze ostrzeżeń dotyczących praw autorskich, nieświadomi, że oprócz brzydkich wyników na starych stronach, zgłaszali również tysiące błędów, po dwa dla każdego wizerunek.

Jeśli możesz zawęzić bardziej szczegółowo wzorzec problemu, a przypadki, w których wynik musiałby zostać zmieniony, mógłbym zobaczyć o podłączeniu go do mojego formatu - co nie jest bardzo skomplikowane i które dla lepszego RegExer niż mógłbym nawet być łatwym. Z drugiej strony nie chciałbym marnować twojego czasu, jeśli takie podejście nie rozwiązałoby problemu.

CK MacLeod
źródło
Dzięki za twoje zdanie na ten temat! Tylko kilka myśli: myślę, że posiadanie niewłaściwych danych w DB i łatanie ich małpami na ekranie nie jest bardzo czystym i zrównoważonym rozwiązaniem. Może to zepsuć się w dowolnym momencie i obniżyć wydajność każdego widoku. Może także mieć nieprzewidywalne skutki uboczne, np. W przypadku innych wtyczek, które analizują lub zmieniają treść w inny sposób. W zależności od tego, jak to się robi, obrazy są nadal uszkodzone w wewnętrznej bazie danych. W takim przypadku myślę, że samo zresetowanie skalowania, wp_constrain_dimensionsjak wspomniano w pytaniu podczas importowania i powstrzymanie się od odbudowywania kciuków, byłoby czystsze.
kraftner
Jesteś bardzo mile widziany. Rzecz w tym, że dane w bazie danych nie są złymi danymi, to po prostu nie są dane, których chcesz już używać w nowym systemie. Jeśli chodzi o osiągi, myślę, że prawdopodobnie byłby minimalny, szczególnie, że teoretycznie dotyczy to tylko postów przed datą X. Mówiąc bardziej ogólnie, może nie istnieć jedno uniwersalne rozwiązanie: Myślę, że dobre wystarczające rozwiązanie może się różnić w zależności od charakteru witryny, wcześniejszych aplikacji i zwyczajów związanych z przetwarzaniem obrazów, wielkości bazy danych, ograniczeń praktycznych i czasowych itd.
CK MacLeod
Prawdopodobnie masz rację, że nie będzie jednego uniwersalnego rozwiązania. Obecnie badam różne sposoby radzenia sobie z tym, w tym podejście renderowania podobne do twojego i podejście importowania, które wolałbym, ponieważ rozwiązuje to raz na zawsze. Zobaczymy, dokąd to prowadzi. :)
kraftner
1

Ok, to jest podstawowe podejście do zastępowania uszkodzonych obrazów w locie. Pamiętaj, że jest to bardziej dowód koncepcji niż rozwiązanie sprawdzone w walce. Po prostu włącza the_contentfiltr, który może (prawdopodobnie ma) pewne niepożądane skutki uboczne w niektórych sytuacjach. Ostrożnie. :)

Chociaż tak też napisano w kodzie, chcę również podziękować @Rarst za tę odpowiedź użytą w moim kodzie.

/*
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
źródło