导出微信公众号文章为PDF

在微信公众号文章页面中添加按钮,点击后导出文章为PDF格式,并显示标题、作者和时间等元信息。

// ==UserScript==
// @name         导出微信公众号文章为PDF
// @namespace    https://github.com/dlzmoe/scripts
// @version      0.5
// @author       dlzmoe
// @description  在微信公众号文章页面中添加按钮,点击后导出文章为PDF格式,并显示标题、作者和时间等元信息。
// @match        https://mp.weixin.qq.com/s/*
// @grant        none
// @license      MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js
// ==/UserScript==

(function () {
  'use strict';

  // 创建一个按钮
  var button = document.createElement('button');
  button.innerHTML = '导出为PDF';
  button.style.position = 'fixed';
  button.style.top = '10px';
  button.style.right = '10px';
  button.style.zIndex = '9999';
  button.style.backgroundColor = '#4CAF50';
  button.style.color = 'white';
  button.style.border = 'none';
  button.style.padding = '10px 20px';
  button.style.fontSize = '16px';
  button.style.cursor = 'pointer';
  button.style.transition = 'background-color 0.3s ease'; // 添加过渡效果
  document.body.appendChild(button);

  // 标志位:防止连续多次点击
  let isExporting = false;

  // 添加加载动画
  function startLoading() {
    button.disabled = true; // 禁用按钮
    button.style.backgroundColor = '#888'; // 变成灰色表示加载中
    button.innerHTML = '正在导出...'; // 更改按钮文本为加载状态
  }

  // 停止加载动画
  function stopLoading() {
    button.disabled = false; // 启用按钮
    button.style.backgroundColor = '#4CAF50'; // 恢复原始颜色
    button.innerHTML = '导出为PDF'; // 恢复按钮文本
  }

  // 点击按钮时执行导出PDF的操作
  button.addEventListener('click', function () {
    if (isExporting) {
      return; // 如果已经在导出过程中,则不允许再次点击
    }

    isExporting = true; // 设置为正在导出
    startLoading(); // 启动加载动画

    // 获取文章内容和标题、作者、时间等元信息
    var article = document.querySelector('.rich_media_content');
    var title = document.querySelector('.rich_media_title');
    var author = document.querySelector('.weui-wa-hotarea'); // 文章作者
    var publishTime = document.querySelector('#publish_time'); // 文章时间

    if (article) {
      // 创建一个容器用于添加元信息
      var metaInfoDiv = document.createElement('div');
      metaInfoDiv.style.marginBottom = '20px';
      metaInfoDiv.style.borderBottom = '1px solid #eee';
      metaInfoDiv.style.paddingBottom = '15px';

      // 标题
      var titleElement = document.createElement('h1');
      titleElement.innerText = title ? title.innerText.trim() : '未命名文章';
      titleElement.style.fontSize = '24px';
      titleElement.style.marginBottom = '10px';
      metaInfoDiv.appendChild(titleElement);

      // 作者
      if (author) {
        var authorElement = document.createElement('p');
        authorElement.innerText = '作者: ' + author.innerText.trim();
        authorElement.style.fontSize = '14px';
        authorElement.style.margin = '5px 0';
        metaInfoDiv.appendChild(authorElement);
      }

      // 时间
      if (publishTime) {
        var timeElement = document.createElement('p');
        timeElement.innerText = '发布时间: ' + publishTime.innerText.trim();
        timeElement.style.fontSize = '14px';
        timeElement.style.margin = '5px 0';
        metaInfoDiv.appendChild(timeElement);
      }

      // 将元信息插入到文章内容的顶部
      article.insertBefore(metaInfoDiv, article.firstChild);

      // 添加防止图片分页的CSS样式
      var style = document.createElement('style');
      style.innerHTML = `
        .rich_media_content img {
          page-break-inside: avoid;
          break-inside: avoid;
          max-width: 100%;
          height: auto;
        }
        .rich_media_content p, .rich_media_content div {
          page-break-inside: avoid;
          break-inside: avoid;
        }
      `;
      document.head.appendChild(style);

      // 确保所有图片加载完成
      let images = article.querySelectorAll('img');
      let imagePromises = [];

      images.forEach(function (img) {
        // 处理懒加载的图片,确保图片的真实 URL 被加载
        if (img.dataset && img.dataset.src) {
          img.src = img.dataset.src;
        }

        // 通过跨域获取图片,并将图片转换为 base64 格式
        imagePromises.push(
          new Promise(function (resolve) {
            var imgElement = new Image();
            imgElement.crossOrigin = 'Anonymous';
            imgElement.src = img.src;
            imgElement.onload = function () {
              var canvas = document.createElement('canvas');
              canvas.width = imgElement.width;
              canvas.height = imgElement.height;
              var ctx = canvas.getContext('2d');
              ctx.drawImage(imgElement, 0, 0);
              img.src = canvas.toDataURL('image/jpeg'); // 使用JPEG格式并压缩质量到70%
              resolve();
            };
            imgElement.onerror = resolve; // 即使图片加载失败,继续处理
          })
        );
      });

      // 确保图片加载完成后再导出PDF
      Promise.all(imagePromises).then(function () {
        // 使用文章标题作为文件名
        var fileName = title ? title.innerText.trim() + '.pdf' : 'WeChat_Article.pdf';

        var opt = {
          margin: 0.5,
          filename: fileName,
          image: {
            type: 'jpeg',
            quality: 1 // 降低图片质量以减小PDF体积
          },
          html2canvas: {
            scale: 1.5, // 降低渲染比例以减小PDF体积
            useCORS: true, // 允许跨域图片
            logging: false, // 关闭日志
            // 可以根据需要添加其他html2canvas选项
          },
          jsPDF: {
            unit: 'in',
            format: 'a4', // 使用A4格式,比letter更常用且体积可能更小
            orientation: 'portrait'
          },
          pagebreak: {
            mode: ['avoid-all', 'css', 'legacy']
          } // 遵循CSS中的page-break规则
        };

        // 使用 html2pdf 将文章内容导出为 PDF
        html2pdf().from(article).set(opt).save().then(function () {
          // 导出完成后,恢复按钮状态
          stopLoading();
          isExporting = false; // 重置导出状态
        }).catch(function (error) {
          alert('导出过程中出现问题: ' + error.message);
          stopLoading(); // 即使出现错误也恢复按钮状态
          isExporting = false; // 重置导出状态
        });
      }).catch(function (error) {
        alert('处理图片时出现问题: ' + error.message);
        stopLoading(); // 即使出现错误也恢复按钮状态
        isExporting = false; // 重置导出状态
      });
    } else {
      alert('未找到文章内容');
      stopLoading(); // 如果未找到文章内容,恢复按钮状态
      isExporting = false; // 重置导出状态
    }
  });
})();