标签: code

  • 【Python】使用PIL库进行多格式批量转换WebP并压缩分辨率

    网站将逐步切换到WebP格式图片,今天捣鼓了插件在服务器端替换图片,但WP的媒体库却怎么都搞不定了,媒体库会自动生成很多缩略图用于不同的场景,我不想碰它的缩略图生成效果,因此只写单一的转换代码是没法做出完整的效果的。

    退而求其次使用本地对图片进行处理,该脚本使用PIL库,图片分辨率限制为2560最长/宽,可以处理带透明通道的图片,也可以处理GIF,用下来效果还不错。

    1月8日更新:

    【Python】使用WebP官方库进行WebP转换
    【Python】使用cwebp、gif2webp、exiftool实现保留exif信息的WebP转换

    import tkinter as tk
    from tkinter import filedialog, messagebox
    from PIL import Image, ImageSequence
    import os
    
    def resize_image(img):
        max_size = 2560
        width, height = img.size
    
        if width > max_size or height > max_size:
            if width > height:
                new_width = max_size
                new_height = int((new_width / width) * height)
            else:
                new_height = max_size
                new_width = int((new_height / height) * width)
            
            img = img.resize((new_width, new_height), Image.LANCZOS)
        
        return img
    
    def convert_to_webp(input_path):
        try:
            file_extension = os.path.splitext(input_path)[1].lower()
            output_path = os.path.splitext(input_path)[0] + ".webp"
    
            if not os.path.exists(input_path):
                raise FileNotFoundError(f"文件 {input_path} 不存在,请检查路径。")
    
            if file_extension == '.webp':
                return f"文件 {input_path} 已是 WebP 格式,无需转换。"
    
            with Image.open(input_path) as img:
                if file_extension in ['.gif'] and getattr(img, "is_animated", False):
                    frames = []
                    durations = []
                    for frame in ImageSequence.Iterator(img):
                        # 处理透明度
                        if frame.mode == "P":
                            frame = frame.convert("RGBA")
                        
                        # 转换帧为 RGBA 并存储
                        new_frame = frame.copy()
                        frames.append(new_frame)
                        durations.append(frame.info.get('duration', 100))
    
                    # 重复最后一帧
                    if len(frames) > 1:
                        durations[-1] = max(durations[-1], 100) 
    
                    # 保存为动态 WebP
                    frames[0].save(
                        output_path,
                        format="WEBP",
                        save_all=True,
                        append_images=frames[1:],
                        duration=durations,
                        loop=img.info.get('loop', 0),  # 循环次数
                        transparency=0,  # 确保透明度保留
                        quality=85
                    )
                else:
                    # 静态图片处理
                    if img.mode == "P":
                        if "transparency" in img.info:
                            img = img.convert("RGBA")
                        else:
                            img = img.convert("RGB")
    
                    img = resize_image(img)
    
                    if img.mode != "RGBA":
                        img = img.convert("RGBA")
    
                    img.save(output_path, format="WEBP", quality=85)
    
            return f"图片已转换并保存为 {output_path}"
        except Exception as e:
            return f"处理文件时发生错误: {e}"
    
    
    def select_files():
        file_paths = filedialog.askopenfilenames(
            title="选择图片文件",
            filetypes=[("所有图片格式", "*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp;*.tiff"), 
                       ("JPEG 图片", "*.jpg;*.jpeg"),
                       ("PNG 图片", "*.png"),
                       ("GIF 图片", "*.gif"),
                       ("WebP 图片", "*.webp"),
                       ("BMP 图片", "*.bmp"),
                       ("TIFF 图片", "*.tiff")]
        )
        if file_paths:
            for path in file_paths:
                file_listbox.insert(tk.END, path)
    
    def convert_and_save_batch():
        files = file_listbox.get(0, tk.END)
        if not files:
            messagebox.showerror("错误", "请选择至少一个图片文件!")
            return
    
        results = []
        for file_path in files:
            result = convert_to_webp(file_path)
            results.append(result)
    
        messagebox.showinfo("完成", "\n".join(results))
    
    def clear_list():
        file_listbox.delete(0, tk.END)
    
    root = tk.Tk()
    root.title("批量图片转换为 WebP 工具")
    root.geometry("600x400")
    
    frame = tk.Frame(root)
    frame.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)
    
    scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL)
    file_listbox = tk.Listbox(frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set)
    scrollbar.config(command=file_listbox.yview)
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
    
    button_frame = tk.Frame(root)
    button_frame.pack(pady=10)
    
    select_button = tk.Button(button_frame, text="选择文件", command=select_files, width=15)
    select_button.grid(row=0, column=0, padx=5)
    
    clear_button = tk.Button(button_frame, text="清空列表", command=clear_list, width=15)
    clear_button.grid(row=0, column=1, padx=5)
    
    convert_button = tk.Button(button_frame, text="批量转换", command=convert_and_save_batch, width=15)
    convert_button.grid(row=0, column=2, padx=5)
    
    root.mainloop()
  • 【自然笔记】通过ECharts建立观鸟图表

    (!该内容目前只做测试,分类未经过严格验证)

    通过模板函数引入ECharts的js,不过这次限制了只有“Echarts”标签的文章加载,避免影响其他界面的加载速度。

    切换图表类型

    效果如上:采用了矩形树图+旭日图,感觉旭日图更加醒目,通过点击标题来切换图表类型。

    <div id="chart-title" style="text-align: center; font-size: 20px; margin-top: 10px; cursor: pointer; color: #007BFF;">
        切换图表类型
    </div>
      document.getElementById('chart-title').addEventListener('click', function () {
                    if (currentOption === 'treemap') {
                        myChart.setOption(sunburstOption);
                        currentOption = 'sunburst';
                    } else {
                        myChart.setOption(treeMapOption);
                        currentOption = 'treemap';
                    }
                });
            });

  • 【WP插件】通过插件形式为WP导入回到顶部功能

    网站此前用的回到顶部按钮是通过插件市场中的插件实现,效果还不错,不过功能比较单一。

    我在其他的博客中看到了一个可以在页面下拉过程中实现当前页面进度的回顶按钮,觉得实用性很棒,因此尝试通过页面脚本的形式导入。

    但是这种方式过于低效,且没法很好自定义,所以便尝试是否可以通过插件的形式导入功能,通过和GPT的一番交流,大致明白了WP插件的制作过程。

    WP的插件可以通过Zip文件导入,其中的结构为:

    backtop/
    ├── assets/
    │ ├── backtop.css
    │ └── backtop.js
    ├── backtop.php
    └── readme.txt (可选)

    其中backtop.php是插件的核心文件,包含了插件的主要功能和初始化代码。

    backtop.css为样式表,js则是JavaScript 文件,写交互逻辑使用。

    参考代码:

    PHP:

    <?php
    /*
    Plugin Name: 写你的插件名称
    Description: 描述
    Version: 版本号
    Author: 作者
    Author URI: https://wanxuefeiyang.cn
    License: GPL2
    */
    
    // Enqueue CSS and JS,注意地址
    function backtop_enqueue_assets() {
        wp_enqueue_style('backtop-style', plugin_dir_url(__FILE__) . 'assets/backtop.css');
        wp_enqueue_script('backtop-script', plugin_dir_url(__FILE__) . 'assets/backtop.js', [], false, true);
    
        $options = get_option('backtop_options');
        wp_localize_script('backtop-script', 'backTopOptions', [
            'position' => $options['position'] ?? 'bottom-right',
            'color' => $options['color'] ?? '#000000',
            'size' => $options['size'] ?? '40px',
            'shape' => $options['shape'] ?? 'circle',
            'margin' => $options['margin'] ?? '20px 20px'
        ]);
    }
    add_action('wp_enqueue_scripts', 'backtop_enqueue_assets');
    
    // 插入 HTML
    function backtop_render_html() {
        echo '
        <div id="backtop-tool">
            <ul>
                <li id="backtop" class="hidden">
                    <span id="backtop-percentage">0%</span>
                </li>
            </ul>
        </div>';
    }
    add_action('wp_footer', 'backtop_render_html');
    
    // 设置页面,可以自定义一些内容
    function backtop_add_settings_page() {
        add_options_page(
            'BackTop Settings',
            'BackTop',
            'manage_options',
            'backtop-settings',
            'backtop_render_settings_page'
        );
    }
    add_action('admin_menu', 'backtop_add_settings_page');
    
    function backtop_render_settings_page() {
        ?>
        <div class="wrap">
            <h1>BackTop Settings</h1>
            <form method="post" action="options.php">
                <?php
                settings_fields('backtop_options_group');
                do_settings_sections('backtop-settings');
                submit_button();
                ?>
            </form>
        </div>
        <?php
    }
    
    function backtop_register_settings() {
        register_setting('backtop_options_group', 'backtop_options', [
            'type' => 'array',
            'sanitize_callback' => 'backtop_sanitize_options',
            'default' => [
                'position' => 'bottom-right',
                'color' => '#000000',
                'size' => '40px',
                'shape' => 'circle',
                'margin' => '20px 20px'
            ],
        ]);
    
        add_settings_section('backtop_main_section', 'Main Settings', null, 'backtop-settings');
    
        add_settings_field('position', 'Position', 'backtop_position_field', 'backtop-settings', 'backtop_main_section');
        add_settings_field('color', 'Background Color', 'backtop_color_field', 'backtop-settings', 'backtop_main_section');
        add_settings_field('size', 'Button Size', 'backtop_size_field', 'backtop-settings', 'backtop_main_section');
        add_settings_field('shape', 'Shape', 'backtop_shape_field', 'backtop-settings', 'backtop_main_section');
        add_settings_field('margin', 'Margin', 'backtop_margin_field', 'backtop-settings', 'backtop_main_section');
    }
    add_action('admin_init', 'backtop_register_settings');
    
    function backtop_position_field() {
        $options = get_option('backtop_options');
        ?>
        <select name="backtop_options[position]">
            <option value="bottom-right" <?php selected($options['position'], 'bottom-right'); ?>>Bottom Right</option>
            <option value="bottom-left" <?php selected($options['position'], 'bottom-left'); ?>>Bottom Left</option>
        </select>
        <?php
    }
    
    function backtop_color_field() {
        $options = get_option('backtop_options');
        ?>
        <input type="color" name="backtop_options[color]" value="<?php echo esc_attr($options['color']); ?>">
        <?php
    }
    
    function backtop_size_field() {
        $options = get_option('backtop_options');
        ?>
        <input type="text" name="backtop_options[size]" value="<?php echo esc_attr($options['size']); ?>" placeholder="e.g., 40px">
        <?php
    }
    
    function backtop_shape_field() {
        $options = get_option('backtop_options');
        ?>
        <select name="backtop_options[shape]">
            <option value="circle" <?php selected($options['shape'], 'circle'); ?>>Circle</option>
            <option value="square" <?php selected($options['shape'], 'square'); ?>>Square</option>
        </select>
        <?php
    }
    
    function backtop_margin_field() {
        $options = get_option('backtop_options');
        ?>
        <input type="text" name="backtop_options[margin]" value="<?php echo esc_attr($options['margin']); ?>" placeholder="e.g., 20px 20px">
        <?php
    }
    
    function backtop_sanitize_options($options) {
        $options['position'] = in_array($options['position'], ['bottom-right', 'bottom-left']) ? $options['position'] : 'bottom-right';
        $options['color'] = sanitize_hex_color($options['color']);
        $options['size'] = preg_match('/^\d+(px|em|%)$/', $options['size']) ? $options['size'] : '40px';
        $options['shape'] = in_array($options['shape'], ['circle', 'square']) ? $options['shape'] : 'circle';
        $options['margin'] = sanitize_text_field($options['margin']);
        return $options;
    }

    JS:

    (function () {
      const backTopTool = document.getElementById("backtop-tool");
      const backTopButton = document.getElementById("backtop");
      const percentageDisplay = document.getElementById("backtop-percentage");
    
      if (backTopTool && backTopButton) {
        const { position, color, size, shape, margin } = backTopOptions || {};
        const [vertical, horizontal] = position.split("-");
        const [marginY, marginX] = margin.split(" ");
        backTopTool.style[vertical] = marginY || "20px";
        backTopTool.style[horizontal] = marginX || "20px";
        backTopButton.style.backgroundColor = color || "#000";
        backTopButton.style.width = size || "40px";
        backTopButton.style.height = size || "40px";
        backTopButton.style.borderRadius = shape === "circle" ? "50%" : "0";
        percentageDisplay.style.fontSize = `${Math.max(parseInt(size) * 0.4, 8)}px`;
      }
    
      const updateScrollProgress = () => {
        const scrollTop = window.scrollY;
        const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
        const scrollPercentage = Math.round((scrollTop / scrollHeight) * 100);
    
        if (percentageDisplay) {
          percentageDisplay.innerHTML = scrollPercentage >= 95 ? "▲" : `${scrollPercentage}%`;
        }
    
        if (backTopButton) {
          backTopButton.classList.toggle("hidden", scrollTop < 200);
        }
      };
    
      const scrollToTop = () => {
        window.scrollTo({ top: 0, behavior: "smooth" });
      };
    
      backTopButton?.addEventListener("click", scrollToTop);
      window.addEventListener("scroll", updateScrollProgress);
    })();

    CSS:

    #backtop-tool {
      position: fixed;
      z-index: 9999;
    }
    
    #backtop-tool ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    #backtop-tool .hidden {
      display: none;
    }
    
    #backtop-tool li {
      display: flex; 
      justify-content: center; /* 水平居中 */
      align-items: center; /* 垂直居中 */
      width: var(--size, 40px);
      height: var(--size, 40px);
      background: var(--color, rgba(0, 0, 0, 0.7));
      color: #fff;
      border-radius: var(--shape, 50%);
      cursor: pointer;
      font-size: calc(var(--size, 40px) * 0.4); /* 根据按钮大小动态调整字体 */
      text-align: center; /* 对齐数字文本 */
      overflow: hidden;
      box-sizing: border-box;
    }

    管理界面如下:

    从插件制作到实现的过程,给我的感觉是通过插件对页面、功能进行修改,可以避免我们干扰WP核心文件,让页面中的其他功能不受影响,对灵活性、安全性都有一定的增强,这对于我这种半桶水来说非常利好,即便哪里设置错了直接把插件删了就好嘛,可玩性很高。