ChatGPT to PDF by PDFCrowd

Turn your chats into neatly formatted PDF.

  1. // ==UserScript==
  2. // @name ChatGPT to PDF by PDFCrowd
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.26
  5. // @description Turn your chats into neatly formatted PDF.
  6. // @author PDFCrowd (https://pdfcrowd.com/)
  7. // @match https://chatgpt.com/*
  8. // @match https://chat.com/*
  9. // @icon64 https://github.com/pdfcrowd/save-chatgpt-as-pdf/raw/master/icons/icon64.png
  10. // @run-at document-end
  11. // @grant GM_xmlhttpRequest
  12. // @connect api.pdfcrowd.com
  13. // @license MIT
  14. // ==/UserScript==
  15. /* globals pdfcrowdChatGPT */
  16.  
  17. // do not modify or delete the following line, it serves as a placeholder for
  18. // the common.js contents which is copied here by "make build-userscript-single-file"
  19. //
  20. // shared.js placeholder
  21. 'use strict';
  22.  
  23. const pdfcrowdShared = {};
  24.  
  25. pdfcrowdShared.defaultOptions = {
  26. margins: '',
  27. theme: '',
  28. zoom: 100,
  29. no_questions: false,
  30. q_color: 'default',
  31. q_color_picker: '#ecf9f2',
  32. q_fg_color: 'default',
  33. q_fg_color_picker: '#000',
  34. title_mode: '',
  35. margin_left: '0.4in',
  36. margin_right: '0.4in',
  37. margin_top: '0.4in',
  38. margin_bottom: '0.4in',
  39. page_break: '',
  40. toc: '',
  41. no_icons: false,
  42. model_name: false
  43. }
  44.  
  45. pdfcrowdShared.version = 'v1.26';
  46.  
  47. pdfcrowdShared.rateUsLink = '#';
  48. pdfcrowdShared.hasOptions = true;
  49. if (typeof GM_info !== 'undefined') {
  50. pdfcrowdShared.rateUsLink = 'https://greasyfork.org/en/scripts/484463-save-chatgpt-as-pdf/feedback#post-discussion';
  51. pdfcrowdShared.hasOptions = false;
  52. } else if (navigator.userAgent.includes('Edg/')) {
  53. pdfcrowdShared.rateUsLink = 'https://microsoftedge.microsoft.com/addons/detail/save-chatgpt-as-pdf/fjlfcopnobjbkjiclieaopipchijelmj';
  54. } else if (navigator.userAgent.includes("Chrome")) {
  55. pdfcrowdShared.rateUsLink = 'https://chromewebstore.google.com/detail/save-chatgpt-as-pdf/ccjfggejcoobknjolglgmfhoeneafhhm/reviews';
  56. } else if (navigator.userAgent.includes("Firefox")) {
  57. pdfcrowdShared.rateUsLink = 'https://addons.mozilla.org/en-US/firefox/addon/save-chatgpt-as-pdf/reviews/';
  58. }
  59.  
  60. pdfcrowdShared.helpContent = `
  61. <div class="pdfcrowd-category-title">
  62. Support
  63. </div>
  64.  
  65. <div style="line-height:1.5">
  66. Feel free to contact us with any questions or for assistance. We're always happy to help!
  67. <br>
  68. Email us at <strong>support@pdfcrowd.com</strong> or use our
  69. <a href="https://pdfcrowd.com/contact/?ref=chatgpt&amp;pr=save-chatgpt-as-pdf-pdfcrowd" title="Contact us" target="_blank">
  70. contact form</a>.
  71. <br>
  72. <span class="popup-hidden">
  73. Please <a href="${pdfcrowdShared.rateUsLink}">rate us</a> if you like the extension. It helps a lot!
  74. </span>
  75. </div>
  76.  
  77. <div class="pdfcrowd-category">
  78. <div class="pdfcrowd-category-title">
  79. Tips
  80. </div>
  81. <ul>
  82. <li>
  83. You can download a specific part of the chat by selecting it.
  84. </li>
  85. <li>
  86. If images are missing in the PDF, reload the page and try downloading the PDF again.
  87. </li>
  88. <li>
  89. Customize the PDF file via addon
  90. <a class="options-link">options</a>.
  91. </li>
  92. </ul>
  93. </div>
  94.  
  95. <div class="pdfcrowd-category">
  96. <div class="pdfcrowd-category-title">
  97. Links
  98. </div>
  99. <ul>
  100. <li>
  101. ChatGPT to PDF by PDFCrowd
  102. <a href="https://pdfcrowd.com/save-chatgpt-as-pdf/" target="_blank">homepage</a>
  103. </li>
  104. <li>
  105. Visit <a href="https://pdfcrowd.com/" target="_blank">PDFCrowd</a>
  106. to learn more about our tool and services.
  107. </li>
  108. <li>
  109. Discover how our
  110. <a href="https://pdfcrowd.com/api/html-to-pdf-api/" target="_blank">HTML to PDF API</a>
  111. can enhance your projects.
  112. </li>
  113. </ul>
  114. </div>
  115. `;
  116.  
  117. pdfcrowdShared.getOptions = function(callback) {
  118. if(typeof chrome === 'undefined') {
  119. callback(pdfcrowdShared.defaultOptions);
  120. } else {
  121. try {
  122. chrome.storage.sync.get('options', function(obj) {
  123. let rv = {};
  124. Object.assign(rv, pdfcrowdShared.defaultOptions);
  125. if(obj.options) {
  126. Object.assign(rv, obj.options);
  127. }
  128. callback(rv);
  129. });
  130. } catch(error) {
  131. console.error(error);
  132. callback(pdfcrowdShared.defaultOptions);
  133. }
  134. }
  135. }
  136.  
  137. function init() {
  138. let elem = document.getElementById('version');
  139. if(elem) {
  140. elem.innerHTML = pdfcrowdShared.version;
  141. }
  142.  
  143. elem = document.getElementById('help');
  144. if(elem) {
  145. elem.innerHTML = pdfcrowdShared.helpContent;
  146. }
  147. }
  148.  
  149. document.addEventListener('DOMContentLoaded', init);
  150. // common.js placeholder
  151. const pdfcrowdChatGPT = {};
  152.  
  153. pdfcrowdChatGPT.pdfcrowdAPI = 'https://api.pdfcrowd.com/convert/24.04/';
  154. pdfcrowdChatGPT.username = 'chat-gpt';
  155. pdfcrowdChatGPT.apiKey = '29d211b1f6924c22b7a799b4e8fecb7e';
  156.  
  157. pdfcrowdChatGPT.init = function() {
  158. if(document.querySelectorAll('.pdfcrowd-convert').length > 0) {
  159. // avoid double init
  160. return;
  161. }
  162.  
  163. // remote images live at least 1 minute
  164. const minImageDuration = 60000;
  165.  
  166. const buttonIconFill = (typeof GM_xmlhttpRequest !== 'undefined')
  167. ? '#A72C16' : '#EA4C3A';
  168.  
  169. const blockStyle = document.createElement('style');
  170. blockStyle.textContent = `
  171. .pdfcrowd-block {
  172. position: fixed;
  173. height: 36px;
  174. top: 10px;
  175. right: 180px;
  176. }
  177.  
  178. @media (max-width: 767px) {
  179. .pdfcrowd-lg {
  180. display: none;
  181. }
  182.  
  183. .pdfcrowd-sm {
  184. display: block;
  185. }
  186. }
  187.  
  188. .pdfcrowd-lg {
  189. display: block;
  190. }
  191.  
  192. .pdfcrowd-sm {
  193. display: none;
  194. }
  195.  
  196. .pdfcrowd-btn-smaller .pdfcrowd-lg {
  197. display: none;
  198. }
  199.  
  200. .pdfcrowd-btn-smaller .pdfcrowd-sm {
  201. display: block;
  202. }
  203.  
  204. .pdfcrowd-btn-smallest .pdfcrowd-lg, .pdfcrowd-btn-smallest .pdfcrowd-sm {
  205. display: none;
  206. }
  207.  
  208. .pdfcrowd-btn-xs-small .pdfcrowd-lg, .pdfcrowd-btn-xs-small .pdfcrowd-sm {
  209. display: none;
  210. }
  211.  
  212. .pdfcrowd-btn-xs-small .btn-small {
  213. background: none;
  214. border: none;
  215. }
  216.  
  217. .pdfcrowd-btn-xs-small svg {
  218. margin: 0;
  219. }
  220.  
  221. svg.pdfcrowd-btn-content {
  222. width: 1rem;
  223. height: 1rem;
  224. }
  225.  
  226. #pdfcrowd-convert-main {
  227. padding-right: 0;
  228. }
  229.  
  230. #pdfcrowd-convert-main:disabled {
  231. cursor: wait;
  232. filter: none;
  233. opacity: 1;
  234. }
  235.  
  236. .pdfcrowd-dropdown-arrow::after {
  237. display: inline-block;
  238. width: 0;
  239. height: 0;
  240. vertical-align: .255em;
  241. content: "";
  242. border-top: .3em solid;
  243. border-right: .3em solid transparent;
  244. border-bottom: 0;
  245. border-left: .3em solid transparent;
  246. }
  247.  
  248. .pdfcrowd-fs-small {
  249. font-size: .875rem;
  250. }
  251.  
  252. #pdfcrowd-more {
  253. cursor: pointer;
  254. padding: .5rem;
  255. border-top-right-radius: .5rem;
  256. border-bottom-right-radius: .5rem;
  257. }
  258.  
  259. #pdfcrowd-more:hover {
  260. background-color: rgba(0,0,0,.1);
  261. }
  262.  
  263. #pdfcrowd-extra-btns {
  264. border: 1px solid rgba(0,0,0,.1);
  265. background-color: #fff;
  266. color: #000;
  267. }
  268.  
  269. .pdfcrowd-extra-btn:hover {
  270. background-color: rgba(0,0,0,.1);
  271. }
  272.  
  273. .pdfcrowd-extra-btn {
  274. width: 100%;
  275. text-align: start;
  276. display: block;
  277. }
  278.  
  279. .pdfcrowd-hidden {
  280. display: none;
  281. }
  282.  
  283. #pdfcrowd-spinner {
  284. position: absolute;
  285. width: 100%;
  286. height: 100%;
  287. }
  288.  
  289. .pdfcrowd-spinner {
  290. border: 4px solid #ccc;
  291. border-radius: 50%;
  292. border-top: 4px solid #ffc107;
  293. width: 1.5rem;
  294. height: 1.5rem;
  295. -webkit-animation: spin 1.5s linear infinite;
  296. animation: spin 1.5s linear infinite;
  297. }
  298.  
  299. @-webkit-keyframes spin {
  300. 0% { -webkit-transform: rotate(0deg); }
  301. 100% { -webkit-transform: rotate(360deg); }
  302. }
  303.  
  304. @keyframes spin {
  305. 0% { transform: rotate(0deg); }
  306. 100% { transform: rotate(360deg); }
  307. }
  308.  
  309. .pdfcrowd-invisible {
  310. visibility: hidden;
  311. }
  312.  
  313. .pdfcrowd-overlay {
  314. z-index: 10000;
  315. display: none;
  316. position: fixed;
  317. top: 0;
  318. left: 0;
  319. width: 100%;
  320. height: 100%;
  321. background: rgba(0, 0, 0, 0.5);
  322. justify-content: center;
  323. align-items: center;
  324. color: #000;
  325. }
  326.  
  327. .pdfcrowd-dialog {
  328. background: #fff;
  329. padding: 0;
  330. margin: 0.5em;
  331. border-radius: 5px;
  332. box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
  333. text-align: start;
  334. }
  335.  
  336. .pdfcrowd-dialog a {
  337. color: revert;
  338. }
  339.  
  340. .pdfcrowd-dialog-body {
  341. padding: 0 2em;
  342. line-height: 2;
  343. }
  344.  
  345. .pdfcrowd-dialog-footer {
  346. text-align: center;
  347. margin: .5em;
  348. position: relative;
  349. }
  350.  
  351. .pdfcrowd-dialog-header {
  352. background-color: #eee;
  353. font-size: 1.25em;
  354. padding: .5em;
  355. border-top-left-radius: 10px;
  356. border-top-right-radius: 10px;
  357. }
  358.  
  359. .pdfcrowd-version {
  360. position: absolute;
  361. bottom: 0;
  362. right: 0;
  363. font-size: .65em;
  364. color: #777;
  365. }
  366.  
  367. .pdfcrowd-dialog ul {
  368. list-style: disc;
  369. margin: 0;
  370. padding: 0 0 0 2em;
  371. }
  372.  
  373. .pdfcrowd-close-x {
  374. cursor: pointer;
  375. float: right;
  376. color: #777;
  377. }
  378.  
  379. #pdfcrowd-help {
  380. cursor: pointer;
  381. }
  382.  
  383. .pdfcrowd-py-1 {
  384. padding-bottom: 0.25rem;
  385. padding-top: 0.25rem;
  386. }
  387.  
  388. .pdfcrowd-px-2 {
  389. padding-left: 0.5rem;
  390. padding-right: 0.5rem;
  391. }
  392.  
  393. .pdfcrowd-mr-1 {
  394. margin-right: 0.25rem;
  395. }
  396.  
  397. .pdfcrowd-mr-4 {
  398. margin-right: 1rem;
  399. }
  400.  
  401. .pdfcrowd-justify-center {
  402. justify-content: center;
  403. }
  404.  
  405. .pdfcrowd-items-center {
  406. align-items: center;
  407. }
  408.  
  409. .pdfcrowd-flex {
  410. display: flex;
  411. }
  412.  
  413. .pdfcrowd-text-left {
  414. text-align: left;
  415. }
  416.  
  417. .pdfcrowd-text-right {
  418. text-align: right;
  419. }
  420.  
  421. .pdfcrowd-h-9 {
  422. height: 2.25rem;
  423. }
  424.  
  425. #pdfcrowd-title {
  426. margin-top: 1em !important;
  427. margin-bottom: .5em !important;
  428. padding: .5em !important;
  429. border: revert !important;
  430. visibility: revert !important;
  431. display: revert !important;
  432. color: revert !important;
  433. background: revert !important;
  434. width: 360px;
  435. border-radius: 5px;
  436. }
  437.  
  438. .pdfcrowd-category {
  439. line-height: normal;
  440. margin-top: 1em;
  441. }
  442.  
  443. .pdfcrowd-category-title {
  444. font-size: larger;
  445. font-weight: bold;
  446. }
  447. `;
  448. document.head.appendChild(blockStyle);
  449.  
  450. const pdfcrowdBlockHtml = `
  451. <button
  452. id="pdfcrowd-convert-main"
  453. type="button"
  454. role="button"
  455. tabindex="0"
  456. aria-label="Save as PDF"
  457. data-conv-options='{"page_size": "a4"}'
  458. class="btn btn-secondary btn-small pdfcrowd-h-9 pdfcrowd-convert pdfcrowd-fs-small">
  459. <svg class="pdfcrowd-mr-1 pdfcrowd-btn-content" version="1.1" viewBox="0 0 30 30" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polyline clip-rule="evenodd" fill="${buttonIconFill}" fill-rule="evenodd" points="30,30 0,30 0,0 30,0 30,30 "/><path d="M15.372,4.377 c0.452,0.213,0.358,0.489,0.219,1.793c-0.142,1.345-0.618,3.802-1.535,6.219c-0.918,2.413-2.28,4.784-3.467,6.539 c-1.186,1.756-2.201,2.897-2.975,3.556c-0.777,0.659-1.314,0.835-1.665,0.893c-0.348,0.058-0.506,0-0.6-0.177 c-0.094-0.176-0.127-0.466-0.046-0.82c0.079-0.35,0.268-0.76,0.804-1.285c0.541-0.527,1.426-1.172,2.661-1.771 c1.235-0.6,2.817-1.156,4.116-1.537c1.299-0.379,2.311-0.585,3.197-0.746c0.888-0.162,1.647-0.277,2.391-0.337 c0.744-0.056,1.474-0.056,2.186,0c0.712,0.06,1.408,0.175,2.011,0.323c0.6,0.146,1.108,0.321,1.551,0.601 c0.442,0.276,0.823,0.657,1.012,1.083c0.192,0.423,0.192,0.893,0.033,1.228c-0.158,0.337-0.476,0.541-0.839,0.66 c-0.364,0.115-0.775,0.144-1.267,0c-0.49-0.148-1.062-0.47-1.662-0.894c-0.601-0.425-1.235-0.952-2.057-1.771 c-0.824-0.819-1.838-1.93-2.692-3.013c-0.854-1.083-1.553-2.136-2.028-3.029c-0.473-0.893-0.727-1.624-0.933-2.355 c-0.206-0.733-0.364-1.464-0.427-2.122S13.326,6.17,13.39,5.701c0.063-0.466,0.16-0.82,0.317-1.055 c0.158-0.23,0.381-0.35,0.539-0.408s0.254-0.058,0.348-0.073c0.094-0.015,0.188-0.044,0.333,0c0.138,0.042,0.321,0.154,0.504,0.268" fill="none" stroke="#FFFFFF" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.4"/></svg>
  460. <div class="pdfcrowd-lg pdfcrowd-btn-content">
  461. Save as PDF
  462. </div>
  463. <div class="pdfcrowd-sm pdfcrowd-btn-content">
  464. PDF
  465. </div>
  466. <div id="pdfcrowd-more" class="pdfcrowd-dropdown-arrow">
  467. </div>
  468. <div id="pdfcrowd-spinner" class="pdfcrowd-hidden">
  469. <div class="pdfcrowd-flex pdfcrowd-justify-center pdfcrowd-items-center pdfcrowd-mr-4" style="height: 100%;">
  470. <div class="pdfcrowd-spinner">
  471. </div>
  472. </div>
  473. </div>
  474. </button>
  475.  
  476. <div id="pdfcrowd-extra-btns" class="pdfcrowd-hidden pdfcrowd-text-left">
  477. <button
  478. id="pdfcrowd-extra-a4p"
  479. type="button"
  480. role="button"
  481. tabindex="0"
  482. aria-label="Save as A4 portrait PDF"
  483. data-conv-options='{"page_size": "a4"}'
  484. class="pdfcrowd-extra-btn pdfcrowd-convert pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  485. A4 Portrait
  486. </button>
  487. <button
  488. id="pdfcrowd-extra-a4l"
  489. type="button"
  490. role="button"
  491. tabindex="0"
  492. aria-label="Save as A4 landscape PDF"
  493. class="pdfcrowd-extra-btn pdfcrowd-convert pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1"
  494. data-conv-options='{"orientation": "landscape", "viewport_width": 1200, "page_size": "a4"}'>
  495. A4 Landscape
  496. </button>
  497. <button
  498. id="pdfcrowd-extra-lp"
  499. type="button"
  500. role="button"
  501. tabindex="0"
  502. aria-label="Save as letter portrait PDF"
  503. data-conv-options='{"page_size": "letter"}'
  504. class="pdfcrowd-extra-btn pdfcrowd-convert pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  505. Letter Portrait
  506. </button>
  507. <button
  508. id="pdfcrowd-extra-ll"
  509. type="button"
  510. role="button"
  511. tabindex="0"
  512. aria-label="Save as letter landscape PDF"
  513. class="pdfcrowd-extra-btn pdfcrowd-convert pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1"
  514. data-conv-options='{"orientation": "landscape", "viewport_width": 1200, "page_size": "letter"}'>
  515. Letter Landscape
  516. </button>
  517. <button
  518. id="pdfcrowd-extra-single-a4p"
  519. type="button"
  520. role="button"
  521. tabindex="0"
  522. aria-label="Save as single page"
  523. data-conv-options='{"page_height": "-1"}'
  524. class="pdfcrowd-extra-btn pdfcrowd-convert pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  525. Single Page
  526. </button>
  527. <hr>
  528. <a id="pdfcrowd-options" href="#"
  529. aria-label="ChatGPT to PDF by PDFCrowd options"
  530. class="pdfcrowd-extra-btn pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  531. Options
  532. </a>
  533. <button
  534. id="pdfcrowd-help"
  535. type="button"
  536. role="button"
  537. aria-label="ChatGPT to PDF by PDFCrowd help"
  538. class="pdfcrowd-extra-btn pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  539. Help
  540. </button>
  541. <a href="${pdfcrowdShared.rateUsLink}" aria-label="Rate the Extension"
  542. class="pdfcrowd-extra-btn pdfcrowd-fs-small pdfcrowd-px-2 pdfcrowd-py-1">
  543. Rate the Extension
  544. </a>
  545. </div>
  546.  
  547. <div class="pdfcrowd-overlay" id="pdfcrowd-error-overlay">
  548. <div class="pdfcrowd-dialog">
  549. <div class="pdfcrowd-dialog-header">
  550. Error occurred
  551. <span class="pdfcrowd-close-x pdfcrowd-close-btn">&times;</span>
  552. </div>
  553. <div class="pdfcrowd-dialog-body" style="text-align: center;">
  554. <p id="pdfcrowd-error-message"></p>
  555. </div>
  556. <div class="pdfcrowd-dialog-footer">
  557. <button class="btn btn-secondary pdfcrowd-close-btn">Close</button>
  558. </div>
  559. </div>
  560. </div>
  561.  
  562. <div class="pdfcrowd-overlay" id="pdfcrowd-title-overlay">
  563. <div class="pdfcrowd-dialog">
  564. <div class="pdfcrowd-dialog-header">
  565. Enter title
  566. <span class="pdfcrowd-close-x pdfcrowd-close-btn">&times;</span>
  567. </div>
  568. <div class="pdfcrowd-dialog-body" style="text-align: center;">
  569. <input id="pdfcrowd-title" name="pdfcrowd-title-ch" autocomplete="off" autocapitalize="off">
  570. </div>
  571. <div class="pdfcrowd-dialog-footer">
  572. <button id="pdfcrowd-title-convert" class="btn btn-secondary"
  573. style="margin-right: .5em">
  574. Save PDF
  575. </button>
  576. <button class="btn btn-secondary pdfcrowd-close-btn"
  577. style="margin-left: .5em">
  578. Cancel
  579. </button>
  580. </div>
  581. </div>
  582. </div>
  583.  
  584. <div class="pdfcrowd-overlay" id="pdfcrowd-help-overlay">
  585. <div class="pdfcrowd-dialog">
  586. <div class="pdfcrowd-dialog-header">
  587. ChatGPT to PDF by PDFCrowd
  588. <span class="pdfcrowd-close-x pdfcrowd-close-btn">&times;</span>
  589. </div>
  590. <div class="pdfcrowd-dialog-body">
  591. ${pdfcrowdShared.helpContent}
  592. </div>
  593.  
  594. <div class="pdfcrowd-dialog-footer">
  595. <button class="btn btn-secondary pdfcrowd-close-btn">Close</button>
  596. <div class="pdfcrowd-version">${pdfcrowdShared.version}</div>
  597. </div>
  598. </div>
  599. </div>
  600. `;
  601.  
  602. function findRow(element) {
  603. return element.closest('article');
  604. }
  605.  
  606. function hasParent(element, parent) {
  607. while(element) {
  608. if(element === parent) {
  609. return true;
  610. }
  611. element = element.parentElement;
  612. }
  613. return false;
  614. }
  615.  
  616. function prepareSelection(element) {
  617. const selection = window.getSelection();
  618. if(!selection.isCollapsed) {
  619. const rangeCount = selection.rangeCount;
  620. if(rangeCount > 0) {
  621. const startElement = findRow(
  622. selection.getRangeAt(0).startContainer.parentElement);
  623. if(startElement && hasParent(startElement, element)) {
  624. // selection is in the main block
  625. const endElement = findRow(
  626. selection.getRangeAt(
  627. rangeCount-1).endContainer.parentElement);
  628.  
  629. const newContainer = document.createElement('main');
  630. newContainer.classList.add('h-full', 'w-full');
  631. let currentElement = startElement;
  632. while(currentElement) {
  633. const child_clone = currentElement.cloneNode(true);
  634. newContainer.appendChild(child_clone);
  635. persistCanvases(currentElement, child_clone);
  636. if(currentElement === endElement) {
  637. break;
  638. }
  639. currentElement = currentElement.nextElementSibling;
  640. }
  641. return newContainer;
  642. }
  643. }
  644. }
  645. let element_clone = element.cloneNode(true);
  646. persistCanvases(element, element_clone);
  647. if(element_clone.tagName.toLowerCase() !== 'main') {
  648. // add main element as it's not presented in a shared chat
  649. const main = document.createElement('main');
  650. main.classList.add('h-full', 'w-full');
  651. main.appendChild(element_clone);
  652. element_clone = main;
  653. }
  654. return element_clone;
  655. }
  656.  
  657. function prepareContent(element) {
  658. element = prepareSelection(element);
  659.  
  660. // fix nested buttons error
  661. element.querySelectorAll('button button').forEach(button => {
  662. button.parentNode.removeChild(button);
  663. });
  664.  
  665. // remove all scripts and styles
  666. element.querySelectorAll('script, style').forEach(el => el.remove());
  667.  
  668. // solve expired images
  669. element.querySelectorAll('.grid img').forEach(img => {
  670. img.setAttribute(
  671. 'alt', 'The image has expired. Refresh ChatGPT page and retry saving to PDF.');
  672. });
  673.  
  674. element.classList.add('chat-gpt-custom');
  675.  
  676. return element;
  677. }
  678.  
  679. function showHelp() {
  680. document.getElementById('pdfcrowd-extra-btns').classList.add(
  681. 'pdfcrowd-hidden');
  682.  
  683. document.getElementById('pdfcrowd-help-overlay').style.display = 'flex';
  684. }
  685.  
  686. function addPdfExtension(filename) {
  687. return filename.replace(/\.*$/, '') + '.pdf';
  688. }
  689.  
  690. function isLight(body) {
  691. return window.getComputedStyle(document.body).backgroundColor != 'rgb(33, 33, 33)';
  692. }
  693.  
  694. function isElementVisible(element) {
  695. const style = window.getComputedStyle(element);
  696. return (
  697. style.display !== 'none' &&
  698. style.visibility !== 'hidden' &&
  699. element.offsetWidth > 0 &&
  700. element.offsetHeight > 0
  701. );
  702. }
  703.  
  704. function styleCanvasArea(element, stop_element) {
  705. while(element) {
  706. if(element == stop_element) {
  707. // canvas parent area not found
  708. return;
  709. }
  710.  
  711. const style_height = element.style.height;
  712. if(style_height &&
  713. style_height !== 'auto' &&
  714. style_height !== 'initial') {
  715. element.style.height = '';
  716. return;
  717. }
  718.  
  719. element = element.parentElement;
  720. }
  721. }
  722.  
  723. function persistCanvases(orig_element, new_element) {
  724. const items = [];
  725. const orig_canvases = orig_element.querySelectorAll('canvas');
  726. const new_canvases = new_element.querySelectorAll('canvas');
  727. if(orig_canvases.length !== new_canvases.length) {
  728. return;
  729. }
  730. for(let i = 0; i < orig_canvases.length; i++) {
  731. const orig_canvas = orig_canvases[i];
  732. if(isElementVisible(orig_canvas)) {
  733. const new_canvas = new_canvases[i];
  734. const img = new_canvas.ownerDocument.createElement('img');
  735. img.src = orig_canvas.toDataURL();
  736. img.classList.add('pdfcrowd-canvas-img');
  737. new_canvas.parentNode.replaceChild(img, new_canvas);
  738.  
  739. styleCanvasArea(img, new_element);
  740. }
  741. }
  742. }
  743.  
  744. function getTitle(main) {
  745. const h1 = main.querySelector('h1');
  746. let title;
  747. if(h1) {
  748. title = h1.textContent;
  749. } else {
  750. const chatTitle = document.querySelector(
  751. `nav a[href="${window.location.pathname}"]`);
  752. title = chatTitle
  753. ? chatTitle.textContent
  754. : document.getElementsByTagName('title')[0].textContent;
  755. }
  756. return title.trim();
  757. }
  758.  
  759. function convert(event) {
  760. pdfcrowdShared.getOptions(function(options) {
  761. let main = document.getElementsByTagName('main');
  762. main = main.length ? main[0] : document.querySelector('div.grow');
  763. const main_clone = prepareContent(main);
  764. const h1 = main_clone.querySelector('h1');
  765.  
  766. let questions = null;
  767. if(options.q_color !== 'default') {
  768. questions = main_clone.querySelectorAll(
  769. '[data-message-author-role="user"]');
  770. const color_val = options.q_color === 'none'
  771. ? 'unset' : options.q_color_picker;
  772. questions.forEach(function(question) {
  773. question.style.backgroundColor = color_val;
  774. if(color_val === 'unset') {
  775. question.style.paddingLeft = 0;
  776. question.style.paddingRight = 0;
  777. }
  778. });
  779. }
  780.  
  781. if(options.q_fg_color !== 'default') {
  782. if(!questions) {
  783. questions = main_clone.querySelectorAll(
  784. '[data-message-author-role="user"]');
  785. }
  786. questions.forEach(function(question) {
  787. question.style.color = options.q_fg_color_picker;
  788. });
  789. }
  790.  
  791. let title = getTitle(main);
  792. let filename = title;
  793.  
  794. function doConvert() {
  795. let trigger = event.target;
  796. document.getElementById('pdfcrowd-extra-btns').classList.add(
  797. 'pdfcrowd-hidden');
  798.  
  799. const btnConvert = document.getElementById(
  800. 'pdfcrowd-convert-main');
  801. btnConvert.disabled = true;
  802. const spinner = document.getElementById('pdfcrowd-spinner');
  803. spinner.classList.remove('pdfcrowd-hidden');
  804. const btnElems = document.getElementsByClassName(
  805. 'pdfcrowd-btn-content');
  806. for(let i = 0; i < btnElems.length; i++) {
  807. btnElems[i].classList.add('pdfcrowd-invisible');
  808. }
  809.  
  810. function cleanup() {
  811. btnConvert.disabled = false;
  812. spinner.classList.add('pdfcrowd-hidden');
  813. for(let i = 0; i < btnElems.length; i++) {
  814. btnElems[i].classList.remove('pdfcrowd-invisible');
  815. }
  816. }
  817.  
  818. const data = {
  819. jpeg_quality: 70,
  820. image_dpi: 150,
  821. convert_images_to_jpeg: 'all',
  822. title: title,
  823. rendering_mode: 'viewport',
  824. smart_scaling_mode: 'viewport-fit'
  825. };
  826.  
  827. if(trigger.id) {
  828. localStorage.setItem('pdfcrowd-btn', trigger.id);
  829. } else {
  830. let lastBtn = localStorage.getItem('pdfcrowd-btn');
  831. if(lastBtn) {
  832. lastBtn = document.getElementById(lastBtn);
  833. if(lastBtn) {
  834. trigger = lastBtn;
  835. }
  836. }
  837. }
  838.  
  839. const convOptions = JSON.parse(
  840. trigger.dataset.convOptions || '{}');
  841.  
  842. for(let key in convOptions) {
  843. data[key] = convOptions[key];
  844. }
  845.  
  846. if(!('viewport_width' in convOptions)) {
  847. data.viewport_width = 800;
  848. }
  849.  
  850. switch(options.margins) {
  851. case 'minimal':
  852. data.no_margins = true;
  853. break;
  854. case 'custom':
  855. data.margin_left = options.margin_left || 0;
  856. data.margin_right = options.margin_right || 0;
  857. data.margin_top = options.margin_top || 0;
  858. data.margin_bottom = options.margin_bottom || 0;
  859. break;
  860. default:
  861. data.margin_bottom = '12px';
  862. }
  863.  
  864. let classes = '';
  865. if(options.theme === 'dark' ||
  866. (options.theme === '' && !isLight(document.body))) {
  867. classes = 'pdfcrowd-dark ';
  868. data.page_background_color = '333333';
  869. }
  870.  
  871. if(options.zoom) {
  872. data.scale_factor = options.zoom;
  873. }
  874.  
  875. if(options.no_questions) {
  876. classes += 'pdfcrowd-no-questions ';
  877. }
  878.  
  879. if(options.no_icons) {
  880. classes += 'pdfcrowd-no-icons ';
  881. }
  882.  
  883. if(options.page_break === 'after') {
  884. classes += 'pdfcrowd-break-after ';
  885. }
  886.  
  887. let toc = '';
  888. if(options.toc && !options.no_questions) {
  889. if(options.toc === 'numbering') {
  890. classes += 'pdfcrowd-use-toc-numbering ';
  891. }
  892. toc = '<div id="pdfcrowd-toc"></div>';
  893. }
  894.  
  895. const h1_style = options.title_mode === 'none' ? 'hidden' : '';
  896.  
  897. let body;
  898. if(h1) {
  899. if(h1_style) {
  900. h1.classList.add(h1_style);
  901. }
  902. if(toc) {
  903. const tocDiv = document.createElement('div');
  904. tocDiv.id = 'pdfcrowd-toc';
  905. h1.insertAdjacentElement('afterend', tocDiv);
  906. }
  907. body = main_clone.outerHTML;
  908. } else {
  909. body = `<h1 class="main-title ${h1_style}">${title}</h1>`
  910. + toc + main_clone.outerHTML;
  911. }
  912.  
  913. function getModelName(element) {
  914. function traverse(node) {
  915. let text = '';
  916.  
  917. node.childNodes.forEach(child => {
  918. let childText = '';
  919. if (child.nodeType === Node.TEXT_NODE) {
  920. childText = child.textContent.trim();
  921. } else if (child.nodeType === Node.ELEMENT_NODE) {
  922. childText = traverse(child);
  923. }
  924.  
  925. if(childText) {
  926. if(text) {
  927. text += ' - ';
  928. }
  929. text += childText;
  930. }
  931. });
  932.  
  933. return text;
  934. }
  935.  
  936. return traverse(element).trim();
  937. }
  938.  
  939. let model_name = '';
  940. if(options.model_name) {
  941. const model_el = document.querySelector(
  942. '#page-header > .flex');
  943. if(model_el) {
  944. model_name = '<div class="pdfcrowd-model-name">' +
  945. getModelName(model_el) +
  946. '</div>';
  947. }
  948. }
  949.  
  950. const direction = document.documentElement.getAttribute(
  951. 'dir') || 'ltr';
  952. data.text = `<!DOCTYPE html><html><head><meta charSet="utf-8"/></head><body class="${classes}" dir="${direction}">${model_name}${body}</body>`;
  953.  
  954. pdfcrowdChatGPT.doRequest(
  955. data, addPdfExtension(filename), cleanup);
  956. }
  957.  
  958. if(options.title_mode === 'ask') {
  959. const dlgTitle = document.getElementById(
  960. 'pdfcrowd-title-overlay');
  961. const titleInput = document.getElementById('pdfcrowd-title');
  962. titleInput.value = title;
  963. dlgTitle.style.display = 'flex';
  964. titleInput.focus();
  965. document.getElementById('pdfcrowd-title-convert')
  966. .onclick = function() {
  967. dlgTitle.style.display = 'none';
  968. title = titleInput.value.trim();
  969. if(title) {
  970. filename = title;
  971. }
  972. // replace h1 if presented is the converted content
  973. if(h1) {
  974. h1.innerText = title;
  975. }
  976. doConvert();
  977. };
  978. } else {
  979. doConvert();
  980. }
  981. });
  982. }
  983.  
  984. function addPdfcrowdBlock() {
  985. const container = document.createElement('div');
  986. container.innerHTML = pdfcrowdBlockHtml;
  987. container.classList.add(
  988. 'pdfcrowd-block', 'pdfcrowd-text-right', 'pdfcrowd-hidden');
  989. document.body.appendChild(container);
  990.  
  991. let buttons = document.querySelectorAll('.pdfcrowd-convert');
  992. buttons.forEach(element => {
  993. element.addEventListener('click', convert);
  994. });
  995.  
  996. document.getElementById('pdfcrowd-help').addEventListener(
  997. 'click', event => {
  998. showHelp();
  999. });
  1000.  
  1001. document.getElementById('pdfcrowd-more').addEventListener('click', event => {
  1002. event.stopPropagation();
  1003. const moreButtons = document.getElementById(
  1004. 'pdfcrowd-extra-btns');
  1005. if(moreButtons.classList.contains('pdfcrowd-hidden')) {
  1006. moreButtons.classList.remove('pdfcrowd-hidden');
  1007. } else {
  1008. moreButtons.classList.add('pdfcrowd-hidden');
  1009. }
  1010. });
  1011.  
  1012. document.addEventListener('click', event => {
  1013. const moreButtons = document.getElementById('pdfcrowd-extra-btns');
  1014.  
  1015. if (!moreButtons.contains(event.target)) {
  1016. moreButtons.classList.add('pdfcrowd-hidden');
  1017. }
  1018. });
  1019.  
  1020. buttons = document.querySelectorAll('.pdfcrowd-close-btn');
  1021. buttons.forEach(element => {
  1022. element.addEventListener('click', () => {
  1023. element.closest('.pdfcrowd-overlay').style.display = 'none';
  1024. });
  1025. });
  1026.  
  1027. return container;
  1028. }
  1029.  
  1030. function isVisible(el) {
  1031. if(el) {
  1032. const style = window.getComputedStyle(el);
  1033. return style.display !== 'none' &&
  1034. style.visibility !== 'hidden' &&
  1035. style.opacity !== '0';
  1036. }
  1037. }
  1038.  
  1039. function areElementsColliding(element1, element2) {
  1040. const rect1 = element1.getBoundingClientRect();
  1041. const rect2 = element2.getBoundingClientRect();
  1042.  
  1043. return !(
  1044. rect1.right < rect2.left ||
  1045. rect1.left > rect2.right ||
  1046. rect1.bottom < rect2.top ||
  1047. rect1.top > rect2.bottom
  1048. );
  1049. }
  1050.  
  1051. const shared_urls = /^https:\/\/chat(gpt)?.com\/share\//;
  1052. const is_shared = shared_urls.test(window.location.href);
  1053. const pdfcrowd_block = addPdfcrowdBlock();
  1054.  
  1055. const BUTTON_MARGIN = 8;
  1056. const WIDTHS = [{
  1057. width: 135,
  1058. cls: null
  1059. }, {
  1060. width: 85,
  1061. cls: 'pdfcrowd-btn-smaller'
  1062. }, {
  1063. width: 58,
  1064. cls: 'pdfcrowd-btn-smallest'
  1065. }, {
  1066. width: 30,
  1067. cls: 'pdfcrowd-btn-xs-small'
  1068. }];
  1069.  
  1070. function getNewPos(elements) {
  1071.  
  1072. for(let i = elements.length - 1; i > 0; i--) {
  1073. const rect1 = elements[i - 1].getBoundingClientRect();
  1074. const rect2 = elements[i].getBoundingClientRect();
  1075.  
  1076. // Calculate horizontal space between the two elements
  1077. const space = rect2.left - (rect1.left + rect1.width);
  1078.  
  1079. for(let j = 0; j < WIDTHS.length; j++) {
  1080. const width = WIDTHS[j];
  1081. if(space >= width.width) {
  1082. return [rect2.left, width.cls];
  1083. }
  1084. }
  1085. }
  1086.  
  1087. return null;
  1088. }
  1089.  
  1090. function getTopBar() {
  1091. const elements = document.querySelectorAll('.draggable.sticky.top-0');
  1092. for(let element of elements) {
  1093. if(isVisible(element)) {
  1094. return element;
  1095. }
  1096. }
  1097.  
  1098. return null;
  1099. }
  1100.  
  1101. let prevClass = null;
  1102.  
  1103. function changeButtonPosition() {
  1104. const topBar = getTopBar();
  1105. if(topBar) {
  1106. // find button position not overlapping anything
  1107. const newPos = getNewPos(topBar.querySelectorAll(':scope > div'));
  1108. if(newPos) {
  1109. const newPosStr = Math.round(
  1110. window.innerWidth - newPos[0] + BUTTON_MARGIN) + 'px';
  1111. const newClass = newPos[1];
  1112. if(newPosStr !== pdfcrowd_block.style.right ||
  1113. prevClass !== newClass) {
  1114. pdfcrowd_block.style.right = newPosStr;
  1115. prevClass = newClass;
  1116. pdfcrowd_block.classList.remove(
  1117. 'pdfcrowd-btn-smaller',
  1118. 'pdfcrowd-btn-smallest',
  1119. 'pdfcrowd-btn-xs-small');
  1120. if(newClass) {
  1121. pdfcrowd_block.classList.add(newClass);
  1122. }
  1123. }
  1124. return;
  1125. }
  1126. }
  1127. pdfcrowd_block.classList.remove(
  1128. 'pdfcrowd-btn-smaller',
  1129. 'pdfcrowd-btn-smallest',
  1130. 'pdfcrowd-btn-xs-small');
  1131. prevClass = null;
  1132. }
  1133.  
  1134. function checkForContent() {
  1135. if(document.querySelector('[data-message-author-role="user"]')) {
  1136. changeButtonPosition();
  1137.  
  1138. pdfcrowd_block.classList.remove('pdfcrowd-hidden');
  1139. // fix conflict with other extensions which remove the button
  1140. if(!pdfcrowd_block.isConnected) {
  1141. console.warn('Extension conflict, another extension deleted PDFCrowd HTML, disable other extensions to fix it.\ncreating the Save as PDF button...');
  1142. document.body.appendChild(pdfcrowd_block);
  1143. }
  1144. if(!blockStyle.isConnected) {
  1145. console.warn('Extension conflict, another extension deleted PDFCrowd HTML, disable other extensions to fix it.\ncreating the button style...');
  1146. document.head.appendChild(blockStyle);
  1147. }
  1148. } else {
  1149. pdfcrowd_block.classList.add('pdfcrowd-hidden');
  1150. }
  1151. }
  1152.  
  1153. const options_el = document.getElementById('pdfcrowd-options');
  1154. if(pdfcrowdShared.hasOptions) {
  1155. options_el.addEventListener('click', function() {
  1156. chrome.runtime.sendMessage({action: "open_options_page"});
  1157. });
  1158. } else {
  1159. options_el.remove();
  1160. }
  1161.  
  1162. setInterval(checkForContent, 1000);
  1163. }
  1164.  
  1165. pdfcrowdChatGPT.showError = function(status, text) {
  1166. let html;
  1167. if (status == 432) {
  1168. html = [
  1169. "<strong>Fair Use Notice</strong><br>",
  1170. "Current usage is over the limit. Please wait a while before trying again.<br><br>",
  1171. ];
  1172. } else {
  1173. html = [];
  1174. if (status) {
  1175. if(status == 'network-error') {
  1176. html.push('Network error while connecting to the conversion service');
  1177. } else {
  1178. html.push(`Code: ${status}`);
  1179. }
  1180. html.push(text);
  1181. html.push('Please try again later');
  1182. } else {
  1183. html.push(text);
  1184. }
  1185. html.push(`If the problem persists, contact us at
  1186. <a href="mailto:support@pdfcrowd.com?subject=ChatGPT%20error">
  1187. support@pdfcrowd.com
  1188. </a>`);
  1189. }
  1190. html = html.join('<br>');
  1191. document.getElementById('pdfcrowd-error-overlay').style.display = 'flex';
  1192. document.getElementById('pdfcrowd-error-message').innerHTML = html;
  1193. };
  1194.  
  1195. pdfcrowdChatGPT.saveBlob = function(url, filename) {
  1196. const a = document.createElement('a');
  1197. a.href = url;
  1198. a.download = filename;
  1199. a.click();
  1200. setTimeout(() => {
  1201. window.URL.revokeObjectURL(url);
  1202. }, 100);
  1203. };
  1204.  
  1205. (function() {
  1206. pdfcrowdChatGPT.doRequest = function(data, fileName, fnCleanup) {
  1207. const formData = new FormData();
  1208. for(let key in data) {
  1209. formData.append(key, data[key]);
  1210. }
  1211. GM_xmlhttpRequest({
  1212. url: pdfcrowdChatGPT.pdfcrowdAPI,
  1213. method: 'POST',
  1214. data: formData,
  1215. responseType: 'blob',
  1216. headers: {
  1217. 'Authorization': 'Basic ' + btoa(
  1218. pdfcrowdChatGPT.username + ':' + pdfcrowdChatGPT.apiKey),
  1219. },
  1220. onload: response => {
  1221. fnCleanup();
  1222. if(response.status == 200) {
  1223. const url = window.URL.createObjectURL(response.response);
  1224. pdfcrowdChatGPT.saveBlob(url, fileName);
  1225. } else {
  1226. pdfcrowdChatGPT.showError(
  1227. response.status, response.responseText);
  1228. }
  1229. },
  1230. onerror: error => {
  1231. console.error('conversion error:', error);
  1232. fnCleanup();
  1233. pdfcrowdChatGPT.showError(500, error.responseText);
  1234. }
  1235. });
  1236. };
  1237.  
  1238. pdfcrowdChatGPT.init();
  1239. })();