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