// ==UserScript==
// @name Save ChatGPT as PDF
// @namespace http://tampermonkey.net/
// @version 1.12
// @description Turn your chats into neatly formatted PDF.
// @author PDFCrowd (https://pdfcrowd.com/)
// @match https://chatgpt.com/*
// @icon64 https://github.com/pdfcrowd/save-chatgpt-as-pdf/raw/master/icons/icon64.png
// @run-at document-end
// @grant GM_xmlhttpRequest
// @connect api.pdfcrowd.com
// @license MIT
// @downloadURL none
// ==/UserScript==
/* globals pdfcrowdChatGPT */
// do not modify or delete the following line, it serves as a placeholder for
// the common.js contents which is copied here by "make build-userscript-single-file"
//
// shared.js placeholder
'use strict';
const pdfcrowdShared = {};
pdfcrowdShared.defaultOptions = {
margins: '',
theme: '',
zoom: 100,
no_questions: false
}
pdfcrowdShared.version = 'v1.12';
pdfcrowdShared.rateUsLink = '#';
pdfcrowdShared.hasOptions = true;
if (typeof GM_info !== 'undefined') {
pdfcrowdShared.rateUsLink = 'https://greasyfork.org/en/scripts/484463-save-chatgpt-as-pdf/feedback#post-discussion';
pdfcrowdShared.hasOptions = false;
} else if (navigator.userAgent.includes("Chrome")) {
pdfcrowdShared.rateUsLink = 'https://chromewebstore.google.com/detail/save-chatgpt-as-pdf/ccjfggejcoobknjolglgmfhoeneafhhm/reviews';
} else if (navigator.userAgent.includes("Firefox")) {
pdfcrowdShared.rateUsLink = 'https://addons.mozilla.org/en-US/firefox/addon/save-chatgpt-as-pdf/reviews/';
}
pdfcrowdShared.helpContent = `
Support
Feel free to contact us with any questions or for assistance. We're always happy to help!
Email us at
support@pdfcrowd.com or use our
contact form .
Tips
You can download a specific part of the chat by selecting it.
If images are missing in the PDF, reload the page and try downloading the PDF again.
Customize the PDF file via addon
options .
`;
pdfcrowdShared.getOptions = function(callback) {
if(typeof chrome === 'undefined') {
callback(pdfcrowdShared.defaultOptions);
} else {
try {
chrome.storage.sync.get('options', function(obj) {
let rv = {};
Object.assign(rv, pdfcrowdShared.defaultOptions);
if(obj.options) {
Object.assign(rv, obj.options);
}
callback(rv);
});
} catch(error) {
console.error(error);
callback(pdfcrowdShared.defaultOptions);
}
}
}
function init() {
let elem = document.getElementById('version');
if(elem) {
elem.innerHTML = pdfcrowdShared.version;
}
elem = document.getElementById('help');
if(elem) {
elem.innerHTML = pdfcrowdShared.helpContent;
}
}
document.addEventListener('DOMContentLoaded', init);
// common.js placeholder
const pdfcrowdChatGPT = {};
pdfcrowdChatGPT.pdfcrowdAPI = 'https://api.pdfcrowd.com/convert/24.04/';
pdfcrowdChatGPT.username = 'chat-gpt';
pdfcrowdChatGPT.apiKey = '29d211b1f6924c22b7a799b4e8fecb7e';
pdfcrowdChatGPT.init = function() {
if(document.querySelectorAll('.pdfcrowd-convert').length > 0) {
// avoid double init
return;
}
// remote images live at least 1 minute
const minImageDuration = 60000;
const buttonIconFill = (typeof GM_xmlhttpRequest !== 'undefined')
? '#A72C16' : '#EA4C3A';
const pdfcrowdBlockHtml = `
Save as PDF
PDF
${pdfcrowdShared.helpContent}
`;
function findRow(element) {
while(element) {
if(element.classList &&
element.classList.contains('text-token-text-primary')) {
return element;
}
element = element.parentElement;
}
return null;
}
function hasParent(element, parent) {
while(element) {
if(element === parent) {
return true;
}
element = element.parentElement;
}
return false;
}
function prepareSelection(element) {
const selection = window.getSelection();
if(!selection.isCollapsed) {
const rangeCount = selection.rangeCount;
if(rangeCount > 0) {
const startElement = findRow(
selection.getRangeAt(0).startContainer.parentElement);
if(startElement && hasParent(startElement, element)) {
// selection is in the main block
const endElement = findRow(
selection.getRangeAt(
rangeCount-1).endContainer.parentElement);
const newContainer = document.createElement('main');
newContainer.classList.add('h-full', 'w-full');
let currentElement = startElement;
while(currentElement) {
newContainer.appendChild(
currentElement.cloneNode(true));
if(currentElement === endElement) {
break;
}
currentElement = currentElement.nextElementSibling;
}
return newContainer;
}
}
}
return element.cloneNode(true);
}
function prepareContent(element) {
element = prepareSelection(element);
// fix nested buttons error
element.querySelectorAll('button button').forEach(button => {
button.parentNode.removeChild(button);
});
// remove all scripts and styles
element.querySelectorAll('script, style').forEach(el => el.remove());
// solve expired images
element.querySelectorAll('.grid img').forEach(img => {
img.setAttribute(
'alt', 'The image has expired. Refresh ChatGPT page and retry saving to PDF.');
});
element.classList.add('chat-gpt-custom');
return element.outerHTML;
}
function showHelp() {
document.getElementById('pdfcrowd-extra-btns').classList.add(
'pdfcrowd-hidden');
document.getElementById('pdfcrowd-help-overlay').style.display = 'flex';
}
function addPdfExtension(filename) {
return filename.replace(/\.*$/, '') + '.pdf';
}
function isLight(body) {
return window.getComputedStyle(document.body).backgroundColor != 'rgb(33, 33, 33)';
}
function convert(event) {
let trigger = event.target;
document.getElementById('pdfcrowd-extra-btns').classList.add(
'pdfcrowd-hidden');
const btnConvert = document.getElementById('pdfcrowd-convert-main');
btnConvert.disabled = true;
const spinner = document.getElementById('pdfcrowd-spinner');
spinner.classList.remove('pdfcrowd-hidden');
const btnElems = document.getElementsByClassName('pdfcrowd-btn-content');
for(let i = 0; i < btnElems.length; i++) {
btnElems[i].classList.add('pdfcrowd-invisible');
}
function cleanup() {
btnConvert.disabled = false;
spinner.classList.add('pdfcrowd-hidden');
for(let i = 0; i < btnElems.length; i++) {
btnElems[i].classList.remove('pdfcrowd-invisible');
}
}
const main = document.getElementsByTagName('main')[0];
const content = prepareContent(main);
let body;
let title = '';
const h1 = main.querySelector('h1');
if(h1) {
title = h1.textContent;
body = content;
} else {
const chatTitle = document.querySelector(`nav a[href="${window.location.pathname}"]`);
title = chatTitle
? chatTitle.textContent
: document.getElementsByTagName('title')[0].textContent;
body = `${title} ` + content;
}
title = title.trim();
const data = {
jpeg_quality: 70,
image_dpi: 150,
convert_images_to_jpeg: 'all',
title: title,
rendering_mode: 'viewport',
smart_scaling_mode: 'viewport-fit'
};
if(trigger.id) {
localStorage.setItem('pdfcrowd-btn', trigger.id);
} else {
let lastBtn = localStorage.getItem('pdfcrowd-btn');
if(lastBtn) {
lastBtn = document.getElementById(lastBtn);
if(lastBtn) {
trigger = lastBtn;
}
}
}
const convOptions = JSON.parse(trigger.dataset.convOptions || '{}');
for(let key in convOptions) {
data[key] = convOptions[key];
}
if(!('viewport_width' in convOptions)) {
data.viewport_width = 800;
}
pdfcrowdShared.getOptions(function(options) {
if(options.margins === 'minimal') {
data.no_margins = true;
} else {
data.margin_bottom = '12px';
}
let classes = '';
if(options.theme === 'dark' ||
(options.theme === '' && !isLight(document.body))) {
classes = 'pdfcrowd-dark ';
data.page_background_color = '333333';
}
if(options.zoom) {
data.scale_factor = options.zoom;
}
if(options.no_questions) {
classes += 'pdfcrowd-no-questions ';
}
data.text = ` ${body}`;
pdfcrowdChatGPT.doRequest(data, addPdfExtension(title), cleanup);
});
}
function addPdfcrowdBlock() {
const container = document.createElement('div');
container.innerHTML = pdfcrowdBlockHtml;
document.body.appendChild(container);
let buttons = document.querySelectorAll('.pdfcrowd-convert');
buttons.forEach(element => {
element.addEventListener('click', convert);
});
document.getElementById('pdfcrowd-help').addEventListener(
'click', event => {
showHelp();
});
document.getElementById('pdfcrowd-more').addEventListener('click', event => {
event.stopPropagation();
const moreButtons = document.getElementById(
'pdfcrowd-extra-btns');
if(moreButtons.classList.contains('pdfcrowd-hidden')) {
moreButtons.classList.remove('pdfcrowd-hidden');
} else {
moreButtons.classList.add('pdfcrowd-hidden');
}
});
document.addEventListener('click', event => {
const moreButtons = document.getElementById('pdfcrowd-extra-btns');
if (!moreButtons.contains(event.target)) {
moreButtons.classList.add('pdfcrowd-hidden');
}
});
buttons = document.querySelectorAll('.pdfcrowd-close-btn');
buttons.forEach(element => {
element.addEventListener('click', () => {
element.closest('.pdfcrowd-overlay').style.display = 'none';
});
});
return container.getElementsByClassName('pdfcrowd-block')[0];
}
const pdfcrowd_block = addPdfcrowdBlock();
function checkForContent() {
if(document.querySelector('main div[role="presentation"]')) {
pdfcrowd_block.classList.remove('pdfcrowd-hidden');
} else {
pdfcrowd_block.classList.add('pdfcrowd-hidden');
}
}
const options_el = document.getElementById('pdfcrowd-options');
if(pdfcrowdShared.hasOptions) {
options_el.addEventListener('click', function() {
chrome.runtime.sendMessage({action: "open_options_page"});
});
} else {
options_el.remove();
}
setInterval(checkForContent, 1000);
}
pdfcrowdChatGPT.showError = function(status, text) {
let html;
if (status == 432) {
html = [
"Fair Use Notice ",
"Current usage is over the limit. Please wait a while before trying again. ",
];
} else {
html = [];
if (status) {
html.push(`Code: ${status}`);
html.push("Please try again later");
} else {
html.push(text);
}
html.push(`If the problem persists, contact us at
support@pdfcrowd.com
`);
}
html = html.join(' ');
document.getElementById('pdfcrowd-error-overlay').style.display = 'flex';
document.getElementById('pdfcrowd-error-message').innerHTML = html;
};
pdfcrowdChatGPT.saveBlob = function(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 100);
};
(function() {
pdfcrowdChatGPT.doRequest = function(data, fileName, fnCleanup) {
const formData = new FormData();
for(let key in data) {
formData.append(key, data[key]);
}
GM_xmlhttpRequest({
url: pdfcrowdChatGPT.pdfcrowdAPI,
method: 'POST',
data: formData,
responseType: 'blob',
headers: {
'Authorization': 'Basic ' + btoa(
pdfcrowdChatGPT.username + ':' + pdfcrowdChatGPT.apiKey),
},
onload: response => {
fnCleanup();
if(response.status == 200) {
const url = window.URL.createObjectURL(response.response);
pdfcrowdChatGPT.saveBlob(url, fileName);
} else {
pdfcrowdChatGPT.showError(
response.status, response.responseText);
}
},
onerror: error => {
console.error('conversion error:', error);
fnCleanup();
pdfcrowdChatGPT.showError(500, error.responseText);
}
});
};
pdfcrowdChatGPT.init();
})();