// ==UserScript==
// @name Save ChatGPT as PDF
// @namespace http://tampermonkey.net/
// @version 1.19
// @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,
q_color: 'default',
q_color_picker: '#ecf9f2',
title_mode: ''
}
pdfcrowdShared.version = 'v1.19';
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 blockStyle = document.createElement('style');
blockStyle.textContent = `
.pdfcrowd-block {
position: fixed;
height: 36px;
top: 10px;
right: 180px;
}
.pdfcrowd-block-login {
top: 50px;
right: 16px;
}
@media (min-width: 768px) {
.pdfcrowd-lg {
display: block;
}
.pdfcrowd-sm {
display: none;
}
}
@media (max-width: 767px) {
.pdfcrowd-block:not(.pdfcrowd-block-login) {
right: 56px;
}
.pdfcrowd-lg {
display: none;
}
.pdfcrowd-sm {
display: block;
}
}
svg.pdfcrowd-btn-content {
width: 1rem;
height: 1rem;
}
#pdfcrowd-convert-main {
padding-right: 0;
}
#pdfcrowd-convert-main:disabled {
cursor: wait;
filter: none;
opacity: 1;
}
.pdfcrowd-dropdown-arrow::after {
display: inline-block;
width: 0;
height: 0;
vertical-align: .255em;
content: "";
border-top: .3em solid;
border-right: .3em solid transparent;
border-bottom: 0;
border-left: .3em solid transparent;
}
.pdfcrowd-fs-small {
font-size: .875rem;
}
#pdfcrowd-more {
cursor: pointer;
padding: .5rem;
border-top-right-radius: .5rem;
border-bottom-right-radius: .5rem;
}
#pdfcrowd-more:hover {
background-color: rgba(0,0,0,.1);
}
#pdfcrowd-extra-btns {
border: 1px solid rgba(0,0,0,.1);
background-color: #fff;
color: #000;
}
.pdfcrowd-extra-btn:hover {
background-color: rgba(0,0,0,.1);
}
.pdfcrowd-extra-btn {
width: 100%;
text-align: start;
display: block;
}
.pdfcrowd-hidden {
display: none;
}
#pdfcrowd-spinner {
position: absolute;
width: 100%;
height: 100%;
}
.pdfcrowd-spinner {
border: 4px solid #ccc;
border-radius: 50%;
border-top: 4px solid #ffc107;
width: 1.5rem;
height: 1.5rem;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.pdfcrowd-invisible {
visibility: hidden;
}
.pdfcrowd-overlay {
z-index: 10000;
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
color: #000;
}
.pdfcrowd-dialog {
background: #fff;
padding: 0;
margin: 0.5em;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
text-align: start;
}
.pdfcrowd-dialog a {
color: revert;
}
.pdfcrowd-dialog-body {
padding: 0 2em;
line-height: 2;
}
.pdfcrowd-dialog-footer {
text-align: center;
margin: .5em;
position: relative;
}
.pdfcrowd-dialog-header {
background-color: #eee;
font-size: 1.25em;
padding: .5em;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.pdfcrowd-version {
position: absolute;
bottom: 0;
right: 0;
font-size: .65em;
color: #777;
}
.pdfcrowd-dialog ul {
list-style: disc;
margin: 0;
padding: 0 0 0 2em;
}
.pdfcrowd-close-x {
cursor: pointer;
float: right;
color: #777;
}
#pdfcrowd-help {
cursor: pointer;
}
.pdfcrowd-py-1 {
padding-bottom: 0.25rem;
padding-top: 0.25rem;
}
.pdfcrowd-px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.pdfcrowd-mr-1 {
margin-right: 0.25rem;
}
.pdfcrowd-mr-4 {
margin-right: 1rem;
}
.pdfcrowd-justify-center {
justify-content: center;
}
.pdfcrowd-items-center {
align-items: center;
}
.pdfcrowd-flex {
display: flex;
}
.pdfcrowd-text-left {
text-align: left;
}
.pdfcrowd-text-right {
text-align: right;
}
.pdfcrowd-h-9 {
height: 2.25rem;
}
#pdfcrowd-title {
margin-top: 1em !important;
margin-bottom: .5em !important;
padding: .5em !important;
border: revert !important;
visibility: revert !important;
display: revert !important;
color: revert !important;
background: revert !important;
width: 360px;
border-radius: 5px;
}
.pdfcrowd-category {
line-height: normal;
margin-top: 1em;
}
.pdfcrowd-category-title {
font-size: larger;
font-weight: bold;
}
`;
document.head.appendChild(blockStyle);
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) {
const child_clone = currentElement.cloneNode(true);
newContainer.appendChild(child_clone);
persistCanvases(currentElement, child_clone);
if(currentElement === endElement) {
break;
}
currentElement = currentElement.nextElementSibling;
}
return newContainer;
}
}
}
let element_clone = element.cloneNode(true);
persistCanvases(element, element_clone);
if(element_clone.tagName.toLowerCase() !== 'main') {
// add main element as it's not presented in a shared chat
const main = document.createElement('main');
main.classList.add('h-full', 'w-full');
main.appendChild(element_clone);
element_clone = main;
}
return element_clone;
}
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;
}
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 isElementVisible(element) {
const style = window.getComputedStyle(element);
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
element.offsetWidth > 0 &&
element.offsetHeight > 0
);
}
function styleCanvasArea(element, stop_element) {
while(element) {
if(element == stop_element) {
// canvas parent area not found
return;
}
const style_height = element.style.height;
if(style_height &&
style_height !== 'auto' &&
style_height !== 'initial') {
element.style.height = '';
return;
}
element = element.parentElement;
}
}
function persistCanvases(orig_element, new_element) {
const items = [];
const orig_canvases = orig_element.querySelectorAll('canvas');
const new_canvases = new_element.querySelectorAll('canvas');
if(orig_canvases.length !== new_canvases.length) {
return;
}
for(let i = 0; i < orig_canvases.length; i++) {
const orig_canvas = orig_canvases[i];
if(isElementVisible(orig_canvas)) {
const new_canvas = new_canvases[i];
const img = new_canvas.ownerDocument.createElement('img');
img.src = orig_canvas.toDataURL();
img.classList.add('pdfcrowd-canvas-img');
new_canvas.parentNode.replaceChild(img, new_canvas);
styleCanvasArea(img, new_element);
}
}
}
function getTitle(main) {
const h1 = main.querySelector('h1');
let title;
if(h1) {
title = h1.textContent;
} else {
const chatTitle = document.querySelector(
`nav a[href="${window.location.pathname}"]`);
title = chatTitle
? chatTitle.textContent
: document.getElementsByTagName('title')[0].textContent;
}
return title.trim();
}
function convert(event) {
pdfcrowdShared.getOptions(function(options) {
let main = document.getElementsByTagName('main');
main = main.length ? main[0] : document.querySelector('div.grow');
const main_clone = prepareContent(main);
const h1 = main_clone.querySelector('h1');
if(options.q_color !== 'default') {
const questions = main_clone.querySelectorAll(
'[data-message-author-role="user"]');
const color_val = options.q_color === 'none'
? 'unset' : options.q_color_picker;
questions.forEach(function(question) {
question.style.backgroundColor = color_val;
if(color_val === 'unset') {
question.style.paddingLeft = 0;
question.style.paddingRight = 0;
}
});
}
let title = getTitle(main);
let filename = title;
function doConvert() {
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 h1_style = options.title_mode === 'none'
? 'hidden' : '';
let body;
if(h1) {
if(h1_style) {
h1.classList.add(h1_style);
}
body = main_clone.outerHTML;
} else {
body = `${title} `
+ main_clone.outerHTML;
}
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;
}
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(filename), cleanup);
}
if(options.title_mode === 'ask') {
const dlgTitle = document.getElementById(
'pdfcrowd-title-overlay');
const titleInput = document.getElementById('pdfcrowd-title');
titleInput.value = title;
dlgTitle.style.display = 'flex';
titleInput.focus();
document.getElementById('pdfcrowd-title-convert')
.onclick = function() {
dlgTitle.style.display = 'none';
title = titleInput.value.trim();
if(title) {
filename = title;
}
// replace h1 if presented is the converted content
if(h1) {
h1.innerText = title;
}
doConvert();
};
} else {
doConvert();
}
});
}
function addPdfcrowdBlock() {
const container = document.createElement('div');
container.innerHTML = pdfcrowdBlockHtml;
container.classList.add(
'pdfcrowd-block', 'pdfcrowd-text-right', 'pdfcrowd-hidden');
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;
}
function isVisible(el) {
if(el) {
const style = window.getComputedStyle(el);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
}
}
const is_shared = window.location.href.startsWith(
"https://chatgpt.com/share/");
const pdfcrowd_block = addPdfcrowdBlock();
function checkForContent() {
if(document.querySelector('main div[role="presentation"]') ||
(is_shared || document.querySelector('div.grow'))) {
pdfcrowd_block.classList.remove('pdfcrowd-hidden');
// fix conflict with other extensions which remove the button
if(!pdfcrowd_block.isConnected) {
console.warn('Extension conflict, another extension deleted PDFCrowd HTML, disable other extensions to fix it.\ncreating the Save as PDF button...');
document.body.appendChild(pdfcrowd_block);
}
if(!blockStyle.isConnected) {
console.warn('Extension conflict, another extension deleted PDFCrowd HTML, disable other extensions to fix it.\ncreating the button style...');
document.head.appendChild(blockStyle);
}
if(isVisible(
document.querySelector('[data-testid="login-button"]'))) {
pdfcrowd_block.classList.add('pdfcrowd-block-login');
}
} 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) {
if(status == 'network-error') {
html.push('Network error while connecting to the conversion service');
} else {
html.push(`Code: ${status}`);
}
html.push(text);
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();
})();