// ==UserScript==
// @name Save ChatGPT as PDF
// @namespace http://tampermonkey.net/
// @version 1.11
// @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.version = 'v1.11';
pdfcrowdShared.rateUsLink = '#';
if (typeof GM_info !== 'undefined') {
pdfcrowdShared.rateUsLink = 'https://greasyfork.org/en/scripts/484463-save-chatgpt-as-pdf/feedback#post-discussion';
} 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.
`;
// 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 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 = {
text: ` ${body}`,
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;
}
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');
}
}
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();
})();