How to remove attachments from main query via pre_get_posts?

Rather than using pre_get_posts hook, you could try a better approach which will work perfectly with parse_request hook. The code –

add_action( 'parse_request', 'my_cpt_parse_request', 11, 2 );
function my_cpt_parse_request( $wp )
{
    if( isset($wp->request) && !is_admin() )
    {
        $requests = explode("https://wordpress.stackexchange.com/", $wp->request);
        $request_amount = count($requests);
        $post_name = array_shift( $requests );

        // if an attachment has been queried under this or another post type, skip checking
        if( $request_amount > 1 && !in_array( $requests['0'], array('feed', 'page') ) )
            return;

        global $wpdb;
        $post = $wpdb->get_row( $wpdb->prepare( 
            "SELECT * FROM $wpdb->posts WHERE post_status="publish" AND post_type=%s AND post_name=%s",
            'flower', 
            $post_name
        ));

        if( $post && isset($post->ID) ){
            $wp->set_query_var( 'flower', $post->post_name );
            $wp->set_query_var( 'post_type', 'flower' );
            $wp->set_query_var( 'name', $post->post_name );
            $wp->set_query_var( 'attachment', false );
            $wp->set_query_var( 'error', false );
            return;
        }
    }
}

Remove the pre_get_post hook and use this one, but keep the post_type_link hook as it is.