feat:Setup Prettier for code formatting
This commit is contained in:
@@ -1,458 +1,468 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background-color: #111827;
|
||||
color: #d1d5db;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background-color: #111827;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
border: 1px solid #374151;
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
border-color: #4f46e5;
|
||||
transform: translateY(-5px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.btn {
|
||||
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Custom file input */
|
||||
input[type="file"]::file-selector-button {
|
||||
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
||||
input[type='file']::file-selector-button {
|
||||
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4f46e5;
|
||||
border-radius: 4px;
|
||||
background: #4f46e5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4338ca;
|
||||
/* indigo-700 */
|
||||
background: #4338ca;
|
||||
/* indigo-700 */
|
||||
}
|
||||
|
||||
/* Style for drag-and-drop placeholder */
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
border: 2px dashed #4f46e5;
|
||||
opacity: 0.4;
|
||||
border: 2px dashed #4f46e5;
|
||||
}
|
||||
|
||||
#embed-pdf-container>div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
#embed-pdf-container > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-interface {
|
||||
color: #39A0ED;
|
||||
color: #39a0ed;
|
||||
}
|
||||
|
||||
.page-thumbnail,
|
||||
#file-list>li {
|
||||
cursor: grab;
|
||||
#file-list > li {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
cursor: grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.compare-viewer-wrapper.overlay-mode {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* This rule now ONLY applies to canvases in overlay mode */
|
||||
.compare-viewer-wrapper.overlay-mode canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.compare-viewer-wrapper.side-by-side-mode {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
height: 75vh;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
height: 75vh;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
|
||||
.pdf-panel canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer a {
|
||||
transition: color 0.2s ease-in-out;
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.marker {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.marker::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/* height: 30px; */
|
||||
background-color: orange;
|
||||
/* Yellow marker color */
|
||||
z-index: -1;
|
||||
transform: skew(-20deg);
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/* height: 30px; */
|
||||
background-color: orange;
|
||||
/* Yellow marker color */
|
||||
z-index: -1;
|
||||
transform: skew(-20deg);
|
||||
}
|
||||
|
||||
.pill {
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
background-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
/* rounded-lg */
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
background-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
/* rounded-lg */
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: #4338ca;
|
||||
/* indigo-700 */
|
||||
background-color: #4338ca;
|
||||
/* indigo-700 */
|
||||
}
|
||||
|
||||
.marker-text {
|
||||
background-color: rgba(255, 255, 0, 0.5);
|
||||
/* Yellow marker color */
|
||||
padding: 0 5px;
|
||||
background-color: rgba(255, 255, 0, 0.5);
|
||||
/* Yellow marker color */
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background-color: #1f2937;
|
||||
/* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
/* rounded-lg */
|
||||
text-align: center;
|
||||
background-color: #1f2937;
|
||||
/* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
/* rounded-lg */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
|
||||
}
|
||||
|
||||
.marker-slanted {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.marker-slanted::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 6px;
|
||||
background: linear-gradient(120deg, #6366f1, #8b5cf6);
|
||||
z-index: -1;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 6px;
|
||||
background: linear-gradient(120deg, #6366f1, #8b5cf6);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #4f46e5, transparent);
|
||||
/* Fades from transparent to indigo and back */
|
||||
margin: 2rem auto;
|
||||
/* my-16 */
|
||||
max-width: 42rem;
|
||||
/* max-w-xl */
|
||||
opacity: 0.5;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #4f46e5, transparent);
|
||||
/* Fades from transparent to indigo and back */
|
||||
margin: 2rem auto;
|
||||
/* my-16 */
|
||||
max-width: 42rem;
|
||||
/* max-w-xl */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 2rem;
|
||||
/* py-3 px-8 */
|
||||
border-radius: 0.5rem;
|
||||
/* rounded-lg */
|
||||
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
|
||||
/* from-indigo-500 to-indigo-600 */
|
||||
color: #ffffff;
|
||||
/* text-white */
|
||||
font-weight: 600;
|
||||
/* font-semibold */
|
||||
transition-property: all;
|
||||
transition-duration: 200ms;
|
||||
transform: translateY(0);
|
||||
display: inline-block;
|
||||
padding: 0.75rem 2rem;
|
||||
/* py-3 px-8 */
|
||||
border-radius: 0.5rem;
|
||||
/* rounded-lg */
|
||||
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
|
||||
/* from-indigo-500 to-indigo-600 */
|
||||
color: #ffffff;
|
||||
/* text-white */
|
||||
font-weight: 600;
|
||||
/* font-semibold */
|
||||
transition-property: all;
|
||||
transition-duration: 200ms;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||
transform: translateY(-0.25rem);
|
||||
/* hover:-translate-y-1 */
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||
0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||
transform: translateY(-0.25rem);
|
||||
/* hover:-translate-y-1 */
|
||||
}
|
||||
|
||||
.btn-gradient:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8;
|
||||
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow:
|
||||
0 0 0 2px #111827,
|
||||
0 0 0 4px #818cf8;
|
||||
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
||||
}
|
||||
|
||||
.btn-gradient:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.solid-spinner {
|
||||
width: 64px;
|
||||
/* w-16 */
|
||||
height: 64px;
|
||||
/* h-16 */
|
||||
border: 5px solid #374151;
|
||||
/* border-gray-700 */
|
||||
border-bottom-color: #4f46e5;
|
||||
/* border-indigo-600 */
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: spin 1s linear infinite;
|
||||
width: 64px;
|
||||
/* w-16 */
|
||||
height: 64px;
|
||||
/* h-16 */
|
||||
border: 5px solid #374151;
|
||||
/* border-gray-700 */
|
||||
border-bottom-color: #4f46e5;
|
||||
/* border-indigo-600 */
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#signature-ghost {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
/* Allows clicks to pass through to the canvas */
|
||||
opacity: 0.6;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
/* Allows clicks to pass through to the canvas */
|
||||
opacity: 0.6;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Highlight for selected signature in the saved list */
|
||||
.saved-signature.selected {
|
||||
border-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
|
||||
border-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
|
||||
}
|
||||
|
||||
/* Cursor change when hovering over a placed signature */
|
||||
#canvas-sign.movable {
|
||||
cursor: move;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#canvas-sign.resize-ns {
|
||||
cursor: ns-resize;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
#canvas-sign.resize-ew {
|
||||
cursor: ew-resize;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
#canvas-sign.resize-nesw {
|
||||
cursor: nesw-resize;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
#canvas-sign.resize-nwse {
|
||||
cursor: nwse-resize;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.faq-item.open .faq-question {
|
||||
color: #818cf8;
|
||||
/* indigo-400 */
|
||||
color: #818cf8;
|
||||
/* indigo-400 */
|
||||
}
|
||||
|
||||
.faq-item.open .faq-icon {
|
||||
transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Testimonial Card Styles */
|
||||
.testimonial-card {
|
||||
background-color: #1f2937;
|
||||
/* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 0.75rem;
|
||||
/* rounded-xl */
|
||||
border: 1px solid #374151;
|
||||
/* border-gray-700 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1f2937;
|
||||
/* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 0.75rem;
|
||||
/* rounded-xl */
|
||||
border: 1px solid #374151;
|
||||
/* border-gray-700 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pill {
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pill {
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
/* Smaller padding and font size by default for mobile */
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-weight: 500;
|
||||
background-color: #374151;
|
||||
/* bg-gray-700 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
/* Smaller padding and font size by default for mobile */
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 9999px;
|
||||
/* rounded-full */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* On small screens (640px) and up, revert to the larger size */
|
||||
@media (min-width: 640px) {
|
||||
.pill {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.pill {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure form-field-group contents don't overflow on small screens */
|
||||
.form-field-group .capitalize {
|
||||
word-break: break-all;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
#form-fields-container {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#form-fields-container {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-field-group {
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
/* Adjust as needed */
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.form-field-group {
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
/* Adjust as needed */
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
#page-merge-preview {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
.legal-content h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.legal-content h3 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: rgb(129 140 248);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: rgb(129 140 248);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.legal-content p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.625;
|
||||
color: rgb(156 163 175);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.625;
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.legal-content ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1rem;
|
||||
color: rgb(156 163 175);
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1rem;
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.legal-content a {
|
||||
color: rgb(129 140 248);
|
||||
text-decoration: underline;
|
||||
color: rgb(129 140 248);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.legal-content a:hover {
|
||||
color: rgb(165 180 252);
|
||||
color: rgb(165 180 252);
|
||||
}
|
||||
|
||||
details>summary {
|
||||
list-style: none;
|
||||
details > summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details>summary .icon {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
details > summary .icon {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
details[open]>summary .icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
details[open] > summary .icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { showLoader, hideLoader, showAlert } from './ui.js';
|
||||
import { state } from './state.js';
|
||||
import { toolLogic } from './logic/index.js';
|
||||
import { icons, createIcons } from "lucide";
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
const editorState = {
|
||||
pdf: null,
|
||||
canvas: null,
|
||||
context: null,
|
||||
container: null,
|
||||
currentPageNum: 1,
|
||||
pageRendering: false,
|
||||
pageNumPending: null,
|
||||
scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
isDrawing: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
cropBoxes: {},
|
||||
lastInteractionRect: null, // Used to store the rectangle from the last move event
|
||||
pdf: null,
|
||||
canvas: null,
|
||||
context: null,
|
||||
container: null,
|
||||
currentPageNum: 1,
|
||||
pageRendering: false,
|
||||
pageNumPending: null,
|
||||
scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
isDrawing: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
cropBoxes: {},
|
||||
lastInteractionRect: null, // Used to store the rectangle from the last move event
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,9 +25,9 @@ const editorState = {
|
||||
* @param {PDFPageProxy} page - The PDF.js page object.
|
||||
*/
|
||||
function calculateFitScale(page: any) {
|
||||
const containerWidth = editorState.container.clientWidth;
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
return containerWidth / viewport.width;
|
||||
const containerWidth = editorState.container.clientWidth;
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
return containerWidth / viewport.width;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,239 +35,269 @@ function calculateFitScale(page: any) {
|
||||
* @param {number} num The page number to render.
|
||||
*/
|
||||
async function renderPage(num: any) {
|
||||
editorState.pageRendering = true;
|
||||
showLoader(`Loading page ${num}...`);
|
||||
editorState.pageRendering = true;
|
||||
showLoader(`Loading page ${num}...`);
|
||||
|
||||
try {
|
||||
const page = await editorState.pdf.getPage(num);
|
||||
try {
|
||||
const page = await editorState.pdf.getPage(num);
|
||||
|
||||
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
||||
if (editorState.scale === 'fit') {
|
||||
editorState.scale = calculateFitScale(page);
|
||||
}
|
||||
|
||||
const viewport = page.getViewport({ scale: editorState.scale });
|
||||
editorState.canvas.height = viewport.height;
|
||||
editorState.canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: editorState.context,
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
|
||||
redrawShapes();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error rendering page:", error);
|
||||
showAlert('Render Error', 'Could not display the page.');
|
||||
} finally {
|
||||
editorState.pageRendering = false;
|
||||
hideLoader();
|
||||
|
||||
document.getElementById('current-page-display').textContent = num;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled = num <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled = num >= editorState.pdf.numPages;
|
||||
|
||||
if (editorState.pageNumPending !== null) {
|
||||
const pendingPage = editorState.pageNumPending;
|
||||
editorState.pageNumPending = null;
|
||||
queueRenderPage(pendingPage);
|
||||
}
|
||||
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
||||
if (editorState.scale === 'fit') {
|
||||
editorState.scale = calculateFitScale(page);
|
||||
}
|
||||
|
||||
const viewport = page.getViewport({ scale: editorState.scale });
|
||||
editorState.canvas.height = viewport.height;
|
||||
editorState.canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: editorState.context,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
editorState.pageSnapshot = editorState.context.getImageData(
|
||||
0,
|
||||
0,
|
||||
editorState.canvas.width,
|
||||
editorState.canvas.height
|
||||
);
|
||||
redrawShapes();
|
||||
} catch (error) {
|
||||
console.error('Error rendering page:', error);
|
||||
showAlert('Render Error', 'Could not display the page.');
|
||||
} finally {
|
||||
editorState.pageRendering = false;
|
||||
hideLoader();
|
||||
|
||||
document.getElementById('current-page-display').textContent = num;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled = num <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled =
|
||||
num >= editorState.pdf.numPages;
|
||||
|
||||
if (editorState.pageNumPending !== null) {
|
||||
const pendingPage = editorState.pageNumPending;
|
||||
editorState.pageNumPending = null;
|
||||
queueRenderPage(pendingPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queueRenderPage(num: any) {
|
||||
if (editorState.pageRendering) {
|
||||
editorState.pageNumPending = num;
|
||||
} else {
|
||||
editorState.currentPageNum = num;
|
||||
renderPage(num);
|
||||
}
|
||||
if (editorState.pageRendering) {
|
||||
editorState.pageNumPending = num;
|
||||
} else {
|
||||
editorState.currentPageNum = num;
|
||||
renderPage(num);
|
||||
}
|
||||
}
|
||||
|
||||
function redrawShapes() {
|
||||
if (editorState.pageSnapshot) {
|
||||
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
||||
}
|
||||
|
||||
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
if (currentCropBox) {
|
||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||
editorState.context.lineWidth = 2;
|
||||
editorState.context.setLineDash([8, 4]);
|
||||
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
|
||||
editorState.context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function getEventCoordinates(e: any) {
|
||||
const rect = editorState.canvas.getBoundingClientRect();
|
||||
const touch = e.touches ? e.touches[0] : e;
|
||||
const scaleX = editorState.canvas.width / rect.width;
|
||||
const scaleY = editorState.canvas.height / rect.height;
|
||||
return {
|
||||
x: (touch.clientX - rect.left) * scaleX,
|
||||
y: (touch.clientY - rect.top) * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function handleInteractionStart(e: any) {
|
||||
e.preventDefault();
|
||||
const coords = getEventCoordinates(e);
|
||||
editorState.isDrawing = true;
|
||||
editorState.startX = coords.x;
|
||||
editorState.startY = coords.y;
|
||||
}
|
||||
|
||||
function handleInteractionMove(e: any) {
|
||||
if (!editorState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
|
||||
redrawShapes();
|
||||
const coords = getEventCoordinates(e);
|
||||
|
||||
const x = Math.min(editorState.startX, coords.x);
|
||||
const y = Math.min(editorState.startY, coords.y);
|
||||
const width = Math.abs(editorState.startX - coords.x);
|
||||
const height = Math.abs(editorState.startY - coords.y);
|
||||
if (editorState.pageSnapshot) {
|
||||
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
||||
}
|
||||
|
||||
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
if (currentCropBox) {
|
||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||
editorState.context.lineWidth = 2;
|
||||
editorState.context.setLineDash([8, 4]);
|
||||
editorState.context.strokeRect(x, y, width, height);
|
||||
editorState.context.strokeRect(
|
||||
currentCropBox.x,
|
||||
currentCropBox.y,
|
||||
currentCropBox.width,
|
||||
currentCropBox.height
|
||||
);
|
||||
editorState.context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last valid rectangle drawn during the move event
|
||||
editorState.lastInteractionRect = { x, y, width, height };
|
||||
function getEventCoordinates(e: any) {
|
||||
const rect = editorState.canvas.getBoundingClientRect();
|
||||
const touch = e.touches ? e.touches[0] : e;
|
||||
const scaleX = editorState.canvas.width / rect.width;
|
||||
const scaleY = editorState.canvas.height / rect.height;
|
||||
return {
|
||||
x: (touch.clientX - rect.left) * scaleX,
|
||||
y: (touch.clientY - rect.top) * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function handleInteractionStart(e: any) {
|
||||
e.preventDefault();
|
||||
const coords = getEventCoordinates(e);
|
||||
editorState.isDrawing = true;
|
||||
editorState.startX = coords.x;
|
||||
editorState.startY = coords.y;
|
||||
}
|
||||
|
||||
function handleInteractionMove(e: any) {
|
||||
if (!editorState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
|
||||
redrawShapes();
|
||||
const coords = getEventCoordinates(e);
|
||||
|
||||
const x = Math.min(editorState.startX, coords.x);
|
||||
const y = Math.min(editorState.startY, coords.y);
|
||||
const width = Math.abs(editorState.startX - coords.x);
|
||||
const height = Math.abs(editorState.startY - coords.y);
|
||||
|
||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||
editorState.context.lineWidth = 2;
|
||||
editorState.context.setLineDash([8, 4]);
|
||||
editorState.context.strokeRect(x, y, width, height);
|
||||
editorState.context.setLineDash([]);
|
||||
|
||||
// Store the last valid rectangle drawn during the move event
|
||||
editorState.lastInteractionRect = { x, y, width, height };
|
||||
}
|
||||
|
||||
function handleInteractionEnd() {
|
||||
if (!editorState.isDrawing) return;
|
||||
editorState.isDrawing = false;
|
||||
if (!editorState.isDrawing) return;
|
||||
editorState.isDrawing = false;
|
||||
|
||||
const finalRect = editorState.lastInteractionRect;
|
||||
const finalRect = editorState.lastInteractionRect;
|
||||
|
||||
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
|
||||
redrawShapes(); // Redraw to clear any invalid, tiny box
|
||||
editorState.lastInteractionRect = null;
|
||||
return;
|
||||
}
|
||||
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
|
||||
redrawShapes(); // Redraw to clear any invalid, tiny box
|
||||
editorState.lastInteractionRect = null;
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
||||
...finalRect,
|
||||
scale: editorState.scale
|
||||
};
|
||||
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
||||
...finalRect,
|
||||
scale: editorState.scale,
|
||||
};
|
||||
|
||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
||||
redrawShapes();
|
||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
||||
redrawShapes();
|
||||
}
|
||||
|
||||
export async function setupCanvasEditor(toolId: any) {
|
||||
editorState.canvas = document.getElementById('canvas-editor');
|
||||
if (!editorState.canvas) return;
|
||||
editorState.container = document.getElementById('canvas-container');
|
||||
editorState.context = editorState.canvas.getContext('2d');
|
||||
editorState.canvas = document.getElementById('canvas-editor');
|
||||
if (!editorState.canvas) return;
|
||||
editorState.container = document.getElementById('canvas-container');
|
||||
editorState.context = editorState.canvas.getContext('2d');
|
||||
|
||||
const pageNav = document.getElementById('page-nav');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
const pageNav = document.getElementById('page-nav');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
editorState.cropBoxes = {};
|
||||
editorState.currentPageNum = 1;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
||||
editorState.scale = 'fit';
|
||||
editorState.cropBoxes = {};
|
||||
editorState.currentPageNum = 1;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
||||
editorState.scale = 'fit';
|
||||
|
||||
pageNav.textContent = '';
|
||||
pageNav.textContent = '';
|
||||
|
||||
const prevButton = document.createElement('button');
|
||||
prevButton.id = 'prev-page';
|
||||
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||
const prevButton = document.createElement('button');
|
||||
prevButton.id = 'prev-page';
|
||||
prevButton.className =
|
||||
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||
|
||||
const pageInfo = document.createElement('span');
|
||||
pageInfo.className = 'text-white font-medium';
|
||||
|
||||
const currentPageDisplay = document.createElement('span');
|
||||
currentPageDisplay.id = 'current-page-display';
|
||||
currentPageDisplay.textContent = '1';
|
||||
const pageInfo = document.createElement('span');
|
||||
pageInfo.className = 'text-white font-medium';
|
||||
|
||||
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
|
||||
const currentPageDisplay = document.createElement('span');
|
||||
currentPageDisplay.id = 'current-page-display';
|
||||
currentPageDisplay.textContent = '1';
|
||||
|
||||
const nextButton = document.createElement('button');
|
||||
nextButton.id = 'next-page';
|
||||
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||
pageInfo.append(
|
||||
'Page ',
|
||||
currentPageDisplay,
|
||||
` of ${editorState.pdf.numPages}`
|
||||
);
|
||||
|
||||
pageNav.append(prevButton, pageInfo, nextButton);
|
||||
const nextButton = document.createElement('button');
|
||||
nextButton.id = 'next-page';
|
||||
nextButton.className =
|
||||
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||
|
||||
createIcons({icons});
|
||||
pageNav.append(prevButton, pageInfo, nextButton);
|
||||
|
||||
document.getElementById('prev-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1);
|
||||
});
|
||||
document.getElementById('next-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
|
||||
});
|
||||
createIcons({ icons });
|
||||
|
||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
||||
const newCanvas = editorState.canvas.cloneNode(true);
|
||||
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
||||
editorState.canvas = newCanvas;
|
||||
editorState.context = newCanvas.getContext('2d');
|
||||
document.getElementById('prev-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum > 1)
|
||||
queueRenderPage(editorState.currentPageNum - 1);
|
||||
});
|
||||
document.getElementById('next-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum < editorState.pdf.numPages)
|
||||
queueRenderPage(editorState.currentPageNum + 1);
|
||||
});
|
||||
|
||||
// Mouse Events
|
||||
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
|
||||
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
|
||||
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
|
||||
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
||||
const newCanvas = editorState.canvas.cloneNode(true);
|
||||
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
||||
editorState.canvas = newCanvas;
|
||||
editorState.context = newCanvas.getContext('2d');
|
||||
|
||||
// Touch Events
|
||||
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
|
||||
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
|
||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||
// Mouse Events
|
||||
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
|
||||
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
|
||||
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
|
||||
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
||||
|
||||
if (toolId === 'crop') {
|
||||
document.getElementById('zoom-in-btn').onclick = () => {
|
||||
editorState.scale += 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('zoom-out-btn').onclick = () => {
|
||||
if (editorState.scale > 0.25) {
|
||||
editorState.scale -= 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
}
|
||||
};
|
||||
document.getElementById('fit-page-btn').onclick = async () => {
|
||||
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
||||
editorState.scale = calculateFitScale(page);
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('clear-crop-btn').onclick = () => {
|
||||
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
redrawShapes();
|
||||
};
|
||||
document.getElementById('clear-all-crops-btn').onclick = () => {
|
||||
editorState.cropBoxes = {};
|
||||
redrawShapes();
|
||||
};
|
||||
// Touch Events
|
||||
editorState.canvas.addEventListener('touchstart', handleInteractionStart, {
|
||||
passive: false,
|
||||
});
|
||||
editorState.canvas.addEventListener('touchmove', handleInteractionMove, {
|
||||
passive: false,
|
||||
});
|
||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||
|
||||
document.getElementById('process-btn').onclick = async () => {
|
||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||
showAlert('No Area Selected', 'Please draw a rectangle on at least one page to select the crop area.');
|
||||
return;
|
||||
}
|
||||
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
|
||||
if (success) {
|
||||
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
|
||||
}
|
||||
};
|
||||
}
|
||||
if (toolId === 'crop') {
|
||||
document.getElementById('zoom-in-btn').onclick = () => {
|
||||
editorState.scale += 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('zoom-out-btn').onclick = () => {
|
||||
if (editorState.scale > 0.25) {
|
||||
editorState.scale -= 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
}
|
||||
};
|
||||
document.getElementById('fit-page-btn').onclick = async () => {
|
||||
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
||||
editorState.scale = calculateFitScale(page);
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('clear-crop-btn').onclick = () => {
|
||||
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
redrawShapes();
|
||||
};
|
||||
document.getElementById('clear-all-crops-btn').onclick = () => {
|
||||
editorState.cropBoxes = {};
|
||||
redrawShapes();
|
||||
};
|
||||
|
||||
queueRenderPage(1);
|
||||
}
|
||||
document.getElementById('process-btn').onclick = async () => {
|
||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||
showAlert(
|
||||
'No Area Selected',
|
||||
'Please draw a rectangle on at least one page to select the crop area.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const success = await toolLogic['crop-pdf'].process(
|
||||
editorState.cropBoxes
|
||||
);
|
||||
if (success) {
|
||||
showAlert(
|
||||
'Success!',
|
||||
'Your PDF has been cropped and the download has started.'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
queueRenderPage(1);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,63 @@
|
||||
export const singlePdfLoadTools = [
|
||||
'split', 'organize', 'rotate', 'add-page-numbers',
|
||||
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
||||
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
||||
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
||||
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
||||
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
||||
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', 'posterize', 'remove-blank-pages'
|
||||
'split',
|
||||
'organize',
|
||||
'rotate',
|
||||
'add-page-numbers',
|
||||
'pdf-to-jpg',
|
||||
'pdf-to-png',
|
||||
'pdf-to-webp',
|
||||
'compress',
|
||||
'pdf-to-greyscale',
|
||||
'edit-metadata',
|
||||
'remove-metadata',
|
||||
'flatten',
|
||||
'delete-pages',
|
||||
'add-blank-page',
|
||||
'extract-pages',
|
||||
'add-watermark',
|
||||
'add-header-footer',
|
||||
'invert-colors',
|
||||
'view-metadata',
|
||||
'reverse-pages',
|
||||
'crop',
|
||||
'redact',
|
||||
'pdf-to-bmp',
|
||||
'pdf-to-tiff',
|
||||
'split-in-half',
|
||||
'page-dimensions',
|
||||
'n-up',
|
||||
'duplicate-organize',
|
||||
'combine-single-page',
|
||||
'fix-dimensions',
|
||||
'change-background-color',
|
||||
'change-text-color',
|
||||
'ocr-pdf',
|
||||
'sign-pdf',
|
||||
'remove-annotations',
|
||||
'cropper',
|
||||
'form-filler',
|
||||
'posterize',
|
||||
'remove-blank-pages',
|
||||
];
|
||||
|
||||
export const simpleTools = [
|
||||
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
'change-permissions',
|
||||
'pdf-to-markdown',
|
||||
'word-to-pdf',
|
||||
];
|
||||
|
||||
export const multiFileTools = [
|
||||
'merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf',
|
||||
'alternate-merge'
|
||||
];
|
||||
'merge',
|
||||
'pdf-to-zip',
|
||||
'jpg-to-pdf',
|
||||
'png-to-pdf',
|
||||
'webp-to-pdf',
|
||||
'image-to-pdf',
|
||||
'svg-to-pdf',
|
||||
'bmp-to-pdf',
|
||||
'heic-to-pdf',
|
||||
'tiff-to-pdf',
|
||||
'alternate-merge',
|
||||
];
|
||||
|
||||
@@ -1,22 +1,104 @@
|
||||
export const tesseractLanguages = {
|
||||
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
|
||||
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
|
||||
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
|
||||
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "dan": "Danish", "deu": "German",
|
||||
"dzo": "Dzongkha", "ell": "Greek, Modern (1453-)", "enm": "English, Middle (1100-1500)", "epo": "Esperanto",
|
||||
"est": "Estonian", "eus": "Basque", "fas": "Persian", "fin": "Finnish", "fra": "French", "frk": "German Fraktur",
|
||||
"frm": "French, Middle (ca. 1400-1600)", "gle": "Irish", "glg": "Galician", "grc": "Greek, Ancient (-1453)",
|
||||
"guj": "Gujarati", "hat": "Haitian; Haitian Creole", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian",
|
||||
"hun": "Hungarian", "iku": "Inuktitut", "ind": "Indonesian", "isl": "Icelandic", "ita": "Italian",
|
||||
"ita_old": "Italian - Old", "jav": "Javanese", "jpn": "Japanese", "kan": "Kannada", "kat": "Georgian",
|
||||
"kat_old": "Georgian - Old", "kaz": "Kazakh", "khm": "Central Khmer", "kir": "Kirghiz; Kyrgyz",
|
||||
"kor": "Korean", "kur": "Kurdish", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian",
|
||||
"mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", "mlt": "Maltese", "msa": "Malay", "mya": "Burmese",
|
||||
"nep": "Nepali", "nld": "Dutch; Flemish", "nor": "Norwegian", "ori": "Oriya", "pan": "Panjabi; Punjabi",
|
||||
"pol": "Polish", "por": "Portuguese", "pus": "Pushto; Pashto", "ron": "Romanian; Moldavian; Moldovan",
|
||||
"rus": "Russian", "san": "Sanskrit", "sin": "Sinhala; Sinhalese", "slk": "Slovak", "slv": "Slovenian",
|
||||
"spa": "Spanish; Castilian", "spa_old": "Spanish; Castilian - Old", "sqi": "Albanian", "srp": "Serbian",
|
||||
"srp_latn": "Serbian - Latin", "swa": "Swahili", "swe": "Swedish", "syr": "Syriac", "tam": "Tamil", "tel": "Telugu",
|
||||
"tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", "tur": "Turkish", "uig": "Uighur; Uyghur",
|
||||
"ukr": "Ukrainian", "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek - Cyrillic", "vie": "Vietnamese", "yid": "Yiddish"
|
||||
};
|
||||
eng: 'English',
|
||||
afr: 'Afrikaans',
|
||||
amh: 'Amharic',
|
||||
ara: 'Arabic',
|
||||
asm: 'Assamese',
|
||||
aze: 'Azerbaijani',
|
||||
aze_cyrl: 'Azerbaijani - Cyrillic',
|
||||
bel: 'Belarusian',
|
||||
ben: 'Bengali',
|
||||
bod: 'Tibetan',
|
||||
bos: 'Bosnian',
|
||||
bul: 'Bulgarian',
|
||||
cat: 'Catalan; Valencian',
|
||||
ceb: 'Cebuano',
|
||||
ces: 'Czech',
|
||||
chi_sim: 'Chinese - Simplified',
|
||||
chi_tra: 'Chinese - Traditional',
|
||||
chr: 'Cherokee',
|
||||
cym: 'Welsh',
|
||||
dan: 'Danish',
|
||||
deu: 'German',
|
||||
dzo: 'Dzongkha',
|
||||
ell: 'Greek, Modern (1453-)',
|
||||
enm: 'English, Middle (1100-1500)',
|
||||
epo: 'Esperanto',
|
||||
est: 'Estonian',
|
||||
eus: 'Basque',
|
||||
fas: 'Persian',
|
||||
fin: 'Finnish',
|
||||
fra: 'French',
|
||||
frk: 'German Fraktur',
|
||||
frm: 'French, Middle (ca. 1400-1600)',
|
||||
gle: 'Irish',
|
||||
glg: 'Galician',
|
||||
grc: 'Greek, Ancient (-1453)',
|
||||
guj: 'Gujarati',
|
||||
hat: 'Haitian; Haitian Creole',
|
||||
heb: 'Hebrew',
|
||||
hin: 'Hindi',
|
||||
hrv: 'Croatian',
|
||||
hun: 'Hungarian',
|
||||
iku: 'Inuktitut',
|
||||
ind: 'Indonesian',
|
||||
isl: 'Icelandic',
|
||||
ita: 'Italian',
|
||||
ita_old: 'Italian - Old',
|
||||
jav: 'Javanese',
|
||||
jpn: 'Japanese',
|
||||
kan: 'Kannada',
|
||||
kat: 'Georgian',
|
||||
kat_old: 'Georgian - Old',
|
||||
kaz: 'Kazakh',
|
||||
khm: 'Central Khmer',
|
||||
kir: 'Kirghiz; Kyrgyz',
|
||||
kor: 'Korean',
|
||||
kur: 'Kurdish',
|
||||
lao: 'Lao',
|
||||
lat: 'Latin',
|
||||
lav: 'Latvian',
|
||||
lit: 'Lithuanian',
|
||||
mal: 'Malayalam',
|
||||
mar: 'Marathi',
|
||||
mkd: 'Macedonian',
|
||||
mlt: 'Maltese',
|
||||
msa: 'Malay',
|
||||
mya: 'Burmese',
|
||||
nep: 'Nepali',
|
||||
nld: 'Dutch; Flemish',
|
||||
nor: 'Norwegian',
|
||||
ori: 'Oriya',
|
||||
pan: 'Panjabi; Punjabi',
|
||||
pol: 'Polish',
|
||||
por: 'Portuguese',
|
||||
pus: 'Pushto; Pashto',
|
||||
ron: 'Romanian; Moldavian; Moldovan',
|
||||
rus: 'Russian',
|
||||
san: 'Sanskrit',
|
||||
sin: 'Sinhala; Sinhalese',
|
||||
slk: 'Slovak',
|
||||
slv: 'Slovenian',
|
||||
spa: 'Spanish; Castilian',
|
||||
spa_old: 'Spanish; Castilian - Old',
|
||||
sqi: 'Albanian',
|
||||
srp: 'Serbian',
|
||||
srp_latn: 'Serbian - Latin',
|
||||
swa: 'Swahili',
|
||||
swe: 'Swedish',
|
||||
syr: 'Syriac',
|
||||
tam: 'Tamil',
|
||||
tel: 'Telugu',
|
||||
tgk: 'Tajik',
|
||||
tgl: 'Tagalog',
|
||||
tha: 'Thai',
|
||||
tir: 'Tigrinya',
|
||||
tur: 'Turkish',
|
||||
uig: 'Uighur; Uyghur',
|
||||
ukr: 'Ukrainian',
|
||||
urd: 'Urdu',
|
||||
uzb: 'Uzbek',
|
||||
uzb_cyrl: 'Uzbek - Cyrillic',
|
||||
vie: 'Vietnamese',
|
||||
yid: 'Yiddish',
|
||||
};
|
||||
|
||||
@@ -1,107 +1,429 @@
|
||||
// This file centralizes the definition of all available tools, organized by category.
|
||||
export const categories = [
|
||||
{
|
||||
name: 'Popular Tools',
|
||||
tools: [
|
||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
|
||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit & Annotate',
|
||||
tools: [
|
||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
|
||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'add-page-numbers', name: 'Page Numbers', icon: 'list-ordered', subtitle: 'Insert page numbers into your document.' },
|
||||
{ id: 'add-watermark', name: 'Add Watermark', icon: 'droplets', subtitle: 'Stamp text or an image over your PDF pages.' },
|
||||
{ id: 'add-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
|
||||
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
|
||||
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
|
||||
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
|
||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
||||
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
|
||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
|
||||
{ id: 'remove-blank-pages', name: 'Remove Blank Pages', icon: 'file-minus-2', subtitle: 'Automatically detect and delete blank pages.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Convert to PDF',
|
||||
tools: [
|
||||
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
|
||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
||||
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
|
||||
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
|
||||
{ id: 'svg-to-pdf', name: 'SVG to PDF', icon: 'pen-tool', subtitle: 'Create a PDF from one or more SVG images.' },
|
||||
{ id: 'bmp-to-pdf', name: 'BMP to PDF', icon: 'image', subtitle: 'Create a PDF from one or more BMP images.' },
|
||||
{ id: 'heic-to-pdf', name: 'HEIC to PDF', icon: 'smartphone', subtitle: 'Create a PDF from one or more HEIC images.' },
|
||||
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
|
||||
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
|
||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
||||
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Convert from PDF',
|
||||
tools: [
|
||||
{ id: 'pdf-to-jpg', name: 'PDF to JPG', icon: 'file-image', subtitle: 'Convert each PDF page into a JPG image.' },
|
||||
{ id: 'pdf-to-png', name: 'PDF to PNG', icon: 'file-image', subtitle: 'Convert each PDF page into a PNG image.' },
|
||||
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
|
||||
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
|
||||
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
|
||||
{ id: 'pdf-to-greyscale', name: 'PDF to Greyscale', icon: 'palette', subtitle: 'Convert all colors to black and white.' },
|
||||
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Organize & Manage',
|
||||
tools: [
|
||||
{ id: 'ocr-pdf', name: 'OCR PDF', icon: 'scan-text', subtitle: 'Make a PDF searchable and copyable.' },
|
||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
||||
{ id: 'alternate-merge', name: 'Alternate & Mix Pages', icon: 'shuffle', subtitle: 'Combine PDFs by alternating pages from each.' },
|
||||
{ id: 'organize', name: 'Organize PDF', icon: 'grip', subtitle: 'Reorder pages by dragging and dropping.' },
|
||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
||||
{ id: 'split-in-half', name: 'Divide Pages', icon: 'table-columns-split', subtitle: 'Divide pages horizontally or vertically.' },
|
||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
||||
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
|
||||
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
|
||||
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
|
||||
{ id: 'n-up', name: 'N-Up PDF', icon: 'layout-grid', subtitle: 'Arrange multiple pages onto a single sheet.' },
|
||||
{ id: 'combine-single-page', name: 'Combine to Single Page', icon: 'unfold-vertical', subtitle: 'Stitch all pages into one continuous scroll.' },
|
||||
{ id: 'view-metadata', name: 'View Metadata', icon: 'info', subtitle: 'Inspect the hidden properties of your PDF.' },
|
||||
{ id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' },
|
||||
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
|
||||
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
|
||||
{ id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Optimize & Repair',
|
||||
tools: [
|
||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
||||
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
|
||||
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Secure PDF',
|
||||
tools: [
|
||||
{ id: 'encrypt', name: 'Encrypt PDF', icon: 'lock', subtitle: 'Add a password to protect your PDF.' },
|
||||
{ id: 'decrypt', name: 'Decrypt PDF', icon: 'unlock', subtitle: 'Remove password protection from a PDF.' },
|
||||
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
|
||||
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
|
||||
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a PDF.' },
|
||||
]
|
||||
},
|
||||
];
|
||||
{
|
||||
name: 'Popular Tools',
|
||||
tools: [
|
||||
{
|
||||
id: 'merge',
|
||||
name: 'Merge PDF',
|
||||
icon: 'combine',
|
||||
subtitle: 'Combine multiple PDFs into one file.',
|
||||
},
|
||||
{
|
||||
id: 'split',
|
||||
name: 'Split PDF',
|
||||
icon: 'scissors',
|
||||
subtitle: 'Extract a range of pages into a new PDF.',
|
||||
},
|
||||
{
|
||||
id: 'compress',
|
||||
name: 'Compress PDF',
|
||||
icon: 'zap',
|
||||
subtitle: 'Reduce the file size of your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
name: 'PDF Editor',
|
||||
icon: 'pocket-knife',
|
||||
subtitle:
|
||||
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
|
||||
},
|
||||
{
|
||||
id: 'jpg-to-pdf',
|
||||
name: 'JPG to PDF',
|
||||
icon: 'image-up',
|
||||
subtitle: 'Create a PDF from one or more JPG images.',
|
||||
},
|
||||
{
|
||||
id: 'sign-pdf',
|
||||
name: 'Sign PDF',
|
||||
icon: 'pen-tool',
|
||||
subtitle: 'Draw, type, or upload your signature.',
|
||||
},
|
||||
{
|
||||
id: 'cropper',
|
||||
name: 'Crop PDF',
|
||||
icon: 'crop',
|
||||
subtitle: 'Trim the margins of every page in your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'extract-pages',
|
||||
name: 'Extract Pages',
|
||||
icon: 'ungroup',
|
||||
subtitle: 'Save a selection of pages as new files.',
|
||||
},
|
||||
{
|
||||
id: 'duplicate-organize',
|
||||
name: 'Duplicate & Organize',
|
||||
icon: 'files',
|
||||
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||
},
|
||||
{
|
||||
id: 'delete-pages',
|
||||
name: 'Delete Pages',
|
||||
icon: 'trash-2',
|
||||
subtitle: 'Remove specific pages from your document.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Edit & Annotate',
|
||||
tools: [
|
||||
{
|
||||
id: 'edit',
|
||||
name: 'PDF Editor',
|
||||
icon: 'pocket-knife',
|
||||
subtitle:
|
||||
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
|
||||
},
|
||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{
|
||||
id: 'add-page-numbers',
|
||||
name: 'Page Numbers',
|
||||
icon: 'list-ordered',
|
||||
subtitle: 'Insert page numbers into your document.',
|
||||
},
|
||||
{
|
||||
id: 'add-watermark',
|
||||
name: 'Add Watermark',
|
||||
icon: 'droplets',
|
||||
subtitle: 'Stamp text or an image over your PDF pages.',
|
||||
},
|
||||
{
|
||||
id: 'add-header-footer',
|
||||
name: 'Header & Footer',
|
||||
icon: 'pilcrow',
|
||||
subtitle: 'Add text to the top and bottom of pages.',
|
||||
},
|
||||
{
|
||||
id: 'invert-colors',
|
||||
name: 'Invert Colors',
|
||||
icon: 'contrast',
|
||||
subtitle: 'Create a "dark mode" version of your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'change-background-color',
|
||||
name: 'Background Color',
|
||||
icon: 'palette',
|
||||
subtitle: 'Change the background color of your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'change-text-color',
|
||||
name: 'Change Text Color',
|
||||
icon: 'type',
|
||||
subtitle: 'Change the color of text in your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'sign-pdf',
|
||||
name: 'Sign PDF',
|
||||
icon: 'pen-tool',
|
||||
subtitle: 'Draw, type, or upload your signature.',
|
||||
},
|
||||
{
|
||||
id: 'remove-annotations',
|
||||
name: 'Remove Annotations',
|
||||
icon: 'eraser',
|
||||
subtitle: 'Strip comments, highlights, and links.',
|
||||
},
|
||||
{
|
||||
id: 'cropper',
|
||||
name: 'Crop PDF',
|
||||
icon: 'crop',
|
||||
subtitle: 'Trim the margins of every page in your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'form-filler',
|
||||
name: 'PDF Form Filler',
|
||||
icon: 'square-pen',
|
||||
subtitle: 'Fill in forms directly in the browser.',
|
||||
},
|
||||
{
|
||||
id: 'remove-blank-pages',
|
||||
name: 'Remove Blank Pages',
|
||||
icon: 'file-minus-2',
|
||||
subtitle: 'Automatically detect and delete blank pages.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Convert to PDF',
|
||||
tools: [
|
||||
{
|
||||
id: 'image-to-pdf',
|
||||
name: 'Image to PDF',
|
||||
icon: 'images',
|
||||
subtitle: 'Combine various images into one PDF.',
|
||||
},
|
||||
{
|
||||
id: 'jpg-to-pdf',
|
||||
name: 'JPG to PDF',
|
||||
icon: 'image-up',
|
||||
subtitle: 'Create a PDF from one or more JPG images.',
|
||||
},
|
||||
{
|
||||
id: 'png-to-pdf',
|
||||
name: 'PNG to PDF',
|
||||
icon: 'image-up',
|
||||
subtitle: 'Create a PDF from one or more PNG images.',
|
||||
},
|
||||
{
|
||||
id: 'webp-to-pdf',
|
||||
name: 'WebP to PDF',
|
||||
icon: 'image-up',
|
||||
subtitle: 'Create a PDF from one or more WebP images.',
|
||||
},
|
||||
{
|
||||
id: 'svg-to-pdf',
|
||||
name: 'SVG to PDF',
|
||||
icon: 'pen-tool',
|
||||
subtitle: 'Create a PDF from one or more SVG images.',
|
||||
},
|
||||
{
|
||||
id: 'bmp-to-pdf',
|
||||
name: 'BMP to PDF',
|
||||
icon: 'image',
|
||||
subtitle: 'Create a PDF from one or more BMP images.',
|
||||
},
|
||||
{
|
||||
id: 'heic-to-pdf',
|
||||
name: 'HEIC to PDF',
|
||||
icon: 'smartphone',
|
||||
subtitle: 'Create a PDF from one or more HEIC images.',
|
||||
},
|
||||
{
|
||||
id: 'tiff-to-pdf',
|
||||
name: 'TIFF to PDF',
|
||||
icon: 'layers',
|
||||
subtitle: 'Create a PDF from one or more TIFF images.',
|
||||
},
|
||||
{
|
||||
id: 'txt-to-pdf',
|
||||
name: 'Text to PDF',
|
||||
icon: 'file-pen',
|
||||
subtitle: 'Convert a plain text file into a PDF.',
|
||||
},
|
||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
||||
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Convert from PDF',
|
||||
tools: [
|
||||
{
|
||||
id: 'pdf-to-jpg',
|
||||
name: 'PDF to JPG',
|
||||
icon: 'file-image',
|
||||
subtitle: 'Convert each PDF page into a JPG image.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-png',
|
||||
name: 'PDF to PNG',
|
||||
icon: 'file-image',
|
||||
subtitle: 'Convert each PDF page into a PNG image.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-webp',
|
||||
name: 'PDF to WebP',
|
||||
icon: 'file-image',
|
||||
subtitle: 'Convert each PDF page into a WebP image.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-bmp',
|
||||
name: 'PDF to BMP',
|
||||
icon: 'file-image',
|
||||
subtitle: 'Convert each PDF page into a BMP image.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-tiff',
|
||||
name: 'PDF to TIFF',
|
||||
icon: 'file-image',
|
||||
subtitle: 'Convert each PDF page into a TIFF image.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-greyscale',
|
||||
name: 'PDF to Greyscale',
|
||||
icon: 'palette',
|
||||
subtitle: 'Convert all colors to black and white.',
|
||||
},
|
||||
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Organize & Manage',
|
||||
tools: [
|
||||
{
|
||||
id: 'ocr-pdf',
|
||||
name: 'OCR PDF',
|
||||
icon: 'scan-text',
|
||||
subtitle: 'Make a PDF searchable and copyable.',
|
||||
},
|
||||
{
|
||||
id: 'merge',
|
||||
name: 'Merge PDF',
|
||||
icon: 'combine',
|
||||
subtitle: 'Combine multiple PDFs into one file.',
|
||||
},
|
||||
{
|
||||
id: 'alternate-merge',
|
||||
name: 'Alternate & Mix Pages',
|
||||
icon: 'shuffle',
|
||||
subtitle: 'Combine PDFs by alternating pages from each.',
|
||||
},
|
||||
{
|
||||
id: 'organize',
|
||||
name: 'Organize PDF',
|
||||
icon: 'grip',
|
||||
subtitle: 'Reorder pages by dragging and dropping.',
|
||||
},
|
||||
{
|
||||
id: 'duplicate-organize',
|
||||
name: 'Duplicate & Organize',
|
||||
icon: 'files',
|
||||
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||
},
|
||||
{
|
||||
id: 'split',
|
||||
name: 'Split PDF',
|
||||
icon: 'scissors',
|
||||
subtitle: 'Extract a range of pages into a new PDF.',
|
||||
},
|
||||
{
|
||||
id: 'split-in-half',
|
||||
name: 'Divide Pages',
|
||||
icon: 'table-columns-split',
|
||||
subtitle: 'Divide pages horizontally or vertically.',
|
||||
},
|
||||
{
|
||||
id: 'extract-pages',
|
||||
name: 'Extract Pages',
|
||||
icon: 'ungroup',
|
||||
subtitle: 'Save a selection of pages as new files.',
|
||||
},
|
||||
{
|
||||
id: 'delete-pages',
|
||||
name: 'Delete Pages',
|
||||
icon: 'trash-2',
|
||||
subtitle: 'Remove specific pages from your document.',
|
||||
},
|
||||
{
|
||||
id: 'add-blank-page',
|
||||
name: 'Add Blank Page',
|
||||
icon: 'file-plus-2',
|
||||
subtitle: 'Insert an empty page anywhere in your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'reverse-pages',
|
||||
name: 'Reverse Pages',
|
||||
icon: 'arrow-down-z-a',
|
||||
subtitle: 'Flip the order of all pages in your document.',
|
||||
},
|
||||
{
|
||||
id: 'rotate',
|
||||
name: 'Rotate PDF',
|
||||
icon: 'rotate-cw',
|
||||
subtitle: 'Turn pages in 90-degree increments.',
|
||||
},
|
||||
{
|
||||
id: 'n-up',
|
||||
name: 'N-Up PDF',
|
||||
icon: 'layout-grid',
|
||||
subtitle: 'Arrange multiple pages onto a single sheet.',
|
||||
},
|
||||
{
|
||||
id: 'combine-single-page',
|
||||
name: 'Combine to Single Page',
|
||||
icon: 'unfold-vertical',
|
||||
subtitle: 'Stitch all pages into one continuous scroll.',
|
||||
},
|
||||
{
|
||||
id: 'view-metadata',
|
||||
name: 'View Metadata',
|
||||
icon: 'info',
|
||||
subtitle: 'Inspect the hidden properties of your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'edit-metadata',
|
||||
name: 'Edit Metadata',
|
||||
icon: 'file-cog',
|
||||
subtitle: 'Change the author, title, and other properties.',
|
||||
},
|
||||
{
|
||||
id: 'pdf-to-zip',
|
||||
name: 'PDFs to ZIP',
|
||||
icon: 'stretch-horizontal',
|
||||
subtitle: 'Package multiple PDF files into a ZIP archive.',
|
||||
},
|
||||
{
|
||||
id: 'compare-pdfs',
|
||||
name: 'Compare PDFs',
|
||||
icon: 'git-compare',
|
||||
subtitle: 'Compare two PDFs side by side.',
|
||||
},
|
||||
{
|
||||
id: 'posterize',
|
||||
name: 'Posterize PDF',
|
||||
icon: 'layout-grid',
|
||||
subtitle: 'Split a large page into multiple smaller pages.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Optimize & Repair',
|
||||
tools: [
|
||||
{
|
||||
id: 'compress',
|
||||
name: 'Compress PDF',
|
||||
icon: 'zap',
|
||||
subtitle: 'Reduce the file size of your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'fix-dimensions',
|
||||
name: 'Fix Page Size',
|
||||
icon: 'ruler-dimension-line',
|
||||
subtitle: 'Standardize all pages to a uniform size.',
|
||||
},
|
||||
{
|
||||
id: 'page-dimensions',
|
||||
name: 'Page Dimensions',
|
||||
icon: 'ruler',
|
||||
subtitle: 'Analyze page size, orientation, and units.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Secure PDF',
|
||||
tools: [
|
||||
{
|
||||
id: 'encrypt',
|
||||
name: 'Encrypt PDF',
|
||||
icon: 'lock',
|
||||
subtitle: 'Add a password to protect your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'decrypt',
|
||||
name: 'Decrypt PDF',
|
||||
icon: 'unlock',
|
||||
subtitle: 'Remove password protection from a PDF.',
|
||||
},
|
||||
{
|
||||
id: 'flatten',
|
||||
name: 'Flatten PDF',
|
||||
icon: 'layers',
|
||||
subtitle: 'Make form fields and annotations non-editable.',
|
||||
},
|
||||
{
|
||||
id: 'remove-metadata',
|
||||
name: 'Remove Metadata',
|
||||
icon: 'file-x',
|
||||
subtitle: 'Strip hidden data from your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'change-permissions',
|
||||
name: 'Change Permissions',
|
||||
icon: 'shield-check',
|
||||
subtitle: 'Set or change user permissions on a PDF.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { state } from '../state.js';
|
||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
||||
import {
|
||||
showLoader,
|
||||
hideLoader,
|
||||
showAlert,
|
||||
renderPageThumbnails,
|
||||
renderFileDisplay,
|
||||
switchView,
|
||||
} from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
@@ -7,352 +14,426 @@ import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.j
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
|
||||
import {
|
||||
multiFileTools,
|
||||
simpleTools,
|
||||
singlePdfLoadTools,
|
||||
} from '../config/pdf-tools.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
async function handleSinglePdfUpload(toolId, file) {
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
hideLoader();
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
if (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
|
||||
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
|
||||
switchView('grid');
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
processBtn.classList.remove('hidden');
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
||||
}
|
||||
|
||||
if (toolId === 'organize' || toolId === 'rotate') {
|
||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||
|
||||
if (toolId === 'rotate') {
|
||||
const rotateAllControls = document.getElementById('rotate-all-controls');
|
||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
||||
|
||||
rotateAllControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
|
||||
const rotateAll = (direction) => {
|
||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
||||
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
|
||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||
const thumbnail = item.querySelector('canvas, img');
|
||||
if (thumbnail) {
|
||||
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
||||
rotateAllRightBtn.onclick = () => rotateAll(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'duplicate-organize') {
|
||||
await renderDuplicateOrganizeThumbnails();
|
||||
}
|
||||
if (['crop', 'redact'].includes(toolId)) {
|
||||
await setupCanvasEditor(toolId);
|
||||
}
|
||||
|
||||
if (toolId === 'view-metadata') {
|
||||
const resultsDiv = document.getElementById('metadata-results');
|
||||
showLoader('Analyzing full PDF metadata...');
|
||||
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
|
||||
const [metadata, fieldObjects] = await Promise.all([
|
||||
pdfjsDoc.getMetadata(),
|
||||
pdfjsDoc.getFieldObjects()
|
||||
]);
|
||||
|
||||
const { info, metadata: rawXmpString } = metadata;
|
||||
|
||||
resultsDiv.textContent = ''; // Clear safely
|
||||
|
||||
const createSection = (title) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mb-4';
|
||||
const h3 = document.createElement('h3');
|
||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||
h3.textContent = title;
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||
wrapper.append(h3, ul);
|
||||
return { wrapper, ul };
|
||||
};
|
||||
|
||||
const createListItem = (key, value) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'flex flex-col sm:flex-row';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
||||
strong.textContent = key;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex-grow text-white break-all';
|
||||
div.textContent = value;
|
||||
li.append(strong, div);
|
||||
return li;
|
||||
};
|
||||
|
||||
const parsePdfDate = (pdfDate) => {
|
||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
||||
try {
|
||||
const year = pdfDate.substring(2, 6);
|
||||
const month = pdfDate.substring(6, 8);
|
||||
const day = pdfDate.substring(8, 10);
|
||||
const hour = pdfDate.substring(10, 12);
|
||||
const minute = pdfDate.substring(12, 14);
|
||||
const second = pdfDate.substring(14, 16);
|
||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
||||
} catch { return pdfDate; }
|
||||
};
|
||||
|
||||
const infoSection = createSection('Info Dictionary');
|
||||
if (info && Object.keys(info).length > 0) {
|
||||
for (const key in info) {
|
||||
let value = info[key] || '- Not Set -';
|
||||
if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
||||
value = parsePdfDate(value);
|
||||
}
|
||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
||||
}
|
||||
} else {
|
||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(infoSection.wrapper);
|
||||
|
||||
const fieldsSection = createSection('Interactive Form Fields');
|
||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||
for (const fieldName in fieldObjects) {
|
||||
const field = fieldObjects[fieldName][0];
|
||||
const value = (field as any).fieldValue || '- Not Set -';
|
||||
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
|
||||
}
|
||||
} else {
|
||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||
|
||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
||||
const xmpContainer = document.createElement('div');
|
||||
xmpContainer.className = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||
if (rawXmpString) {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||
pre.textContent = String(rawXmpString);
|
||||
xmpContainer.appendChild(pre);
|
||||
} else {
|
||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||
}
|
||||
xmpSection.wrapper.appendChild(xmpContainer);
|
||||
resultsDiv.appendChild(xmpSection.wrapper);
|
||||
|
||||
resultsDiv.classList.remove('hidden');
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to view metadata or fields:", e);
|
||||
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'edit-metadata') {
|
||||
const form = document.getElementById('metadata-form');
|
||||
const container = document.getElementById('custom-metadata-container');
|
||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||
|
||||
const formatDateForInput = (date) => {
|
||||
if (!date) return '';
|
||||
const pad = (num) => num.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
(document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || '';
|
||||
(document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
|
||||
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
|
||||
(document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || '';
|
||||
(document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || '';
|
||||
(document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || '';
|
||||
(document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||
(document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate());
|
||||
|
||||
addBtn.onclick = () => {
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key (e.g., Department)';
|
||||
keyInput.className = 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||
valueInput.className = 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||
|
||||
fieldWrapper.append(keyInput, valueInput, removeBtn);
|
||||
container.appendChild(fieldWrapper);
|
||||
createIcons({ icons });
|
||||
};
|
||||
|
||||
form.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
if (toolId === 'cropper') {
|
||||
document.getElementById('cropper-ui-container').classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (toolId === 'page-dimensions') {
|
||||
toolLogic['page-dimensions']();
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
} catch (e) {
|
||||
hideLoader();
|
||||
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
|
||||
console.error(e);
|
||||
if (
|
||||
state.pdfDoc.isEncrypted &&
|
||||
toolId !== 'decrypt' &&
|
||||
toolId !== 'change-permissions'
|
||||
) {
|
||||
showAlert(
|
||||
'Protected PDF',
|
||||
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
|
||||
);
|
||||
switchView('grid');
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsDiv = document.querySelector(
|
||||
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
|
||||
);
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
processBtn.classList.remove('hidden');
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func =
|
||||
typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'split',
|
||||
'delete-pages',
|
||||
'add-blank-page',
|
||||
'extract-pages',
|
||||
'add-header-footer',
|
||||
].includes(toolId)
|
||||
) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||
.getPageCount()
|
||||
.toString();
|
||||
}
|
||||
|
||||
if (toolId === 'organize' || toolId === 'rotate') {
|
||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||
|
||||
if (toolId === 'rotate') {
|
||||
const rotateAllControls = document.getElementById(
|
||||
'rotate-all-controls'
|
||||
);
|
||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||
const rotateAllRightBtn = document.getElementById(
|
||||
'rotate-all-right-btn'
|
||||
);
|
||||
|
||||
rotateAllControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
|
||||
const rotateAll = (direction) => {
|
||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||
const currentRotation = parseInt(
|
||||
(item as HTMLElement).dataset.rotation || '0'
|
||||
);
|
||||
const newRotation = (currentRotation + direction * 90 + 360) % 360;
|
||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||
const thumbnail = item.querySelector('canvas, img');
|
||||
if (thumbnail) {
|
||||
(thumbnail as HTMLElement).style.transform =
|
||||
`rotate(${newRotation}deg)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
||||
rotateAllRightBtn.onclick = () => rotateAll(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'duplicate-organize') {
|
||||
await renderDuplicateOrganizeThumbnails();
|
||||
}
|
||||
if (['crop', 'redact'].includes(toolId)) {
|
||||
await setupCanvasEditor(toolId);
|
||||
}
|
||||
|
||||
if (toolId === 'view-metadata') {
|
||||
const resultsDiv = document.getElementById('metadata-results');
|
||||
showLoader('Analyzing full PDF metadata...');
|
||||
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||
data: pdfBytes as ArrayBuffer,
|
||||
}).promise;
|
||||
const [metadata, fieldObjects] = await Promise.all([
|
||||
pdfjsDoc.getMetadata(),
|
||||
pdfjsDoc.getFieldObjects(),
|
||||
]);
|
||||
|
||||
const { info, metadata: rawXmpString } = metadata;
|
||||
|
||||
resultsDiv.textContent = ''; // Clear safely
|
||||
|
||||
const createSection = (title) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mb-4';
|
||||
const h3 = document.createElement('h3');
|
||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||
h3.textContent = title;
|
||||
const ul = document.createElement('ul');
|
||||
ul.className =
|
||||
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||
wrapper.append(h3, ul);
|
||||
return { wrapper, ul };
|
||||
};
|
||||
|
||||
const createListItem = (key, value) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'flex flex-col sm:flex-row';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
||||
strong.textContent = key;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex-grow text-white break-all';
|
||||
div.textContent = value;
|
||||
li.append(strong, div);
|
||||
return li;
|
||||
};
|
||||
|
||||
const parsePdfDate = (pdfDate) => {
|
||||
if (
|
||||
!pdfDate ||
|
||||
typeof pdfDate !== 'string' ||
|
||||
!pdfDate.startsWith('D:')
|
||||
)
|
||||
return pdfDate;
|
||||
try {
|
||||
const year = pdfDate.substring(2, 6);
|
||||
const month = pdfDate.substring(6, 8);
|
||||
const day = pdfDate.substring(8, 10);
|
||||
const hour = pdfDate.substring(10, 12);
|
||||
const minute = pdfDate.substring(12, 14);
|
||||
const second = pdfDate.substring(14, 16);
|
||||
return new Date(
|
||||
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
|
||||
).toLocaleString();
|
||||
} catch {
|
||||
return pdfDate;
|
||||
}
|
||||
};
|
||||
|
||||
const infoSection = createSection('Info Dictionary');
|
||||
if (info && Object.keys(info).length > 0) {
|
||||
for (const key in info) {
|
||||
let value = info[key] || '- Not Set -';
|
||||
if (
|
||||
(key === 'CreationDate' || key === 'ModDate') &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = parsePdfDate(value);
|
||||
}
|
||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
||||
}
|
||||
} else {
|
||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(infoSection.wrapper);
|
||||
|
||||
const fieldsSection = createSection('Interactive Form Fields');
|
||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||
for (const fieldName in fieldObjects) {
|
||||
const field = fieldObjects[fieldName][0];
|
||||
const value = (field as any).fieldValue || '- Not Set -';
|
||||
fieldsSection.ul.appendChild(
|
||||
createListItem(fieldName, String(value))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||
|
||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
||||
const xmpContainer = document.createElement('div');
|
||||
xmpContainer.className =
|
||||
'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||
if (rawXmpString) {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||
pre.textContent = String(rawXmpString);
|
||||
xmpContainer.appendChild(pre);
|
||||
} else {
|
||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||
}
|
||||
xmpSection.wrapper.appendChild(xmpContainer);
|
||||
resultsDiv.appendChild(xmpSection.wrapper);
|
||||
|
||||
resultsDiv.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error('Failed to view metadata or fields:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'edit-metadata') {
|
||||
const form = document.getElementById('metadata-form');
|
||||
const container = document.getElementById('custom-metadata-container');
|
||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||
|
||||
const formatDateForInput = (date) => {
|
||||
if (!date) return '';
|
||||
const pad = (num) => num.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
(document.getElementById('meta-title') as HTMLInputElement).value =
|
||||
state.pdfDoc.getTitle() || '';
|
||||
(document.getElementById('meta-author') as HTMLInputElement).value =
|
||||
state.pdfDoc.getAuthor() || '';
|
||||
(document.getElementById('meta-subject') as HTMLInputElement).value =
|
||||
state.pdfDoc.getSubject() || '';
|
||||
(document.getElementById('meta-keywords') as HTMLInputElement).value =
|
||||
state.pdfDoc.getKeywords() || '';
|
||||
(document.getElementById('meta-creator') as HTMLInputElement).value =
|
||||
state.pdfDoc.getCreator() || '';
|
||||
(document.getElementById('meta-producer') as HTMLInputElement).value =
|
||||
state.pdfDoc.getProducer() || '';
|
||||
(
|
||||
document.getElementById('meta-creation-date') as HTMLInputElement
|
||||
).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||
(document.getElementById('meta-mod-date') as HTMLInputElement).value =
|
||||
formatDateForInput(state.pdfDoc.getModificationDate());
|
||||
|
||||
addBtn.onclick = () => {
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key (e.g., Department)';
|
||||
keyInput.className =
|
||||
'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||
valueInput.className =
|
||||
'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className =
|
||||
'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||
|
||||
fieldWrapper.append(keyInput, valueInput, removeBtn);
|
||||
container.appendChild(fieldWrapper);
|
||||
createIcons({ icons });
|
||||
};
|
||||
|
||||
form.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
if (toolId === 'cropper') {
|
||||
document
|
||||
.getElementById('cropper-ui-container')
|
||||
.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (toolId === 'page-dimensions') {
|
||||
toolLogic['page-dimensions']();
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
} catch (e) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not load PDF. The file may be invalid, corrupted, or password-protected.'
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMultiFileUpload(toolId) {
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'merge') {
|
||||
toolLogic.merge.setup();
|
||||
} else if (toolId === 'alternate-merge') {
|
||||
toolLogic['alternate-merge'].setup();
|
||||
} else if (toolId === 'image-to-pdf') {
|
||||
const imageList = document.getElementById('image-list');
|
||||
imageList.textContent = ''; // Clear safely
|
||||
if (toolId === 'merge') {
|
||||
toolLogic.merge.setup();
|
||||
} else if (toolId === 'alternate-merge') {
|
||||
toolLogic['alternate-merge'].setup();
|
||||
} else if (toolId === 'image-to-pdf') {
|
||||
const imageList = document.getElementById('image-list');
|
||||
imageList.textContent = ''; // Clear safely
|
||||
|
||||
state.files.forEach(file => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const li = document.createElement('li');
|
||||
li.className = "relative group cursor-move";
|
||||
li.dataset.fileName = file.name;
|
||||
state.files.forEach((file) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const li = document.createElement('li');
|
||||
li.className = 'relative group cursor-move';
|
||||
li.dataset.fileName = file.name;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.className =
|
||||
'w-full h-full object-cover rounded-md border-2 border-gray-600';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
|
||||
p.textContent = file.name; // Safe insertion
|
||||
const p = document.createElement('p');
|
||||
p.className =
|
||||
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1';
|
||||
p.textContent = file.name; // Safe insertion
|
||||
|
||||
li.append(img, p);
|
||||
imageList.appendChild(li);
|
||||
});
|
||||
li.append(img, p);
|
||||
imageList.appendChild(li);
|
||||
});
|
||||
|
||||
Sortable.create(imageList);
|
||||
}
|
||||
Sortable.create(imageList);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupFileInputHandler(toolId) {
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||
let isFirstUpload = true;
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||
let isFirstUpload = true;
|
||||
|
||||
const processFiles = async (newFiles) => {
|
||||
if (newFiles.length === 0) return;
|
||||
const processFiles = async (newFiles) => {
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
if (!isMultiFileTool || isFirstUpload) {
|
||||
state.files = newFiles;
|
||||
} else {
|
||||
state.files = [...state.files, ...newFiles];
|
||||
}
|
||||
isFirstUpload = false;
|
||||
if (!isMultiFileTool || isFirstUpload) {
|
||||
state.files = newFiles;
|
||||
} else {
|
||||
state.files = [...state.files, ...newFiles];
|
||||
}
|
||||
isFirstUpload = false;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
renderFileDisplay(fileDisplayArea, state.files);
|
||||
}
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
renderFileDisplay(fileDisplayArea, state.files);
|
||||
}
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
}
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
if (isMultiFileTool) {
|
||||
handleMultiFileUpload(toolId);
|
||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||
await handleSinglePdfUpload(toolId, state.files[0]);
|
||||
} else if (simpleTools.includes(toolId)) {
|
||||
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
|
||||
const optionsDiv = document.getElementById(optionsDivId);
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
processBtn.onclick = () => {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
func();
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (toolId === 'edit') {
|
||||
const file = state.files[0];
|
||||
if (!file) return;
|
||||
if (isMultiFileTool) {
|
||||
handleMultiFileUpload(toolId);
|
||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||
await handleSinglePdfUpload(toolId, state.files[0]);
|
||||
} else if (simpleTools.includes(toolId)) {
|
||||
const optionsDivId =
|
||||
toolId === 'change-permissions'
|
||||
? 'permissions-options'
|
||||
: `${toolId}-options`;
|
||||
const optionsDiv = document.getElementById(optionsDivId);
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
processBtn.onclick = () => {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func =
|
||||
typeof logic.process === 'function' ? logic.process : logic;
|
||||
func();
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (toolId === 'edit') {
|
||||
const file = state.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||
|
||||
pdfContainer.textContent = ''; // Clear safely
|
||||
pdfContainer.textContent = ''; // Clear safely
|
||||
|
||||
if (state.currentPdfUrl) {
|
||||
URL.revokeObjectURL(state.currentPdfUrl);
|
||||
}
|
||||
pdfWrapper.classList.remove('hidden');
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
state.currentPdfUrl = fileURL;
|
||||
if (state.currentPdfUrl) {
|
||||
URL.revokeObjectURL(state.currentPdfUrl);
|
||||
}
|
||||
pdfWrapper.classList.remove('hidden');
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
state.currentPdfUrl = fileURL;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.textContent = `
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.textContent = `
|
||||
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
||||
EmbedPDF.init({
|
||||
type: 'container',
|
||||
@@ -361,55 +442,62 @@ export function setupFileInputHandler(toolId) {
|
||||
theme: 'dark',
|
||||
});
|
||||
`;
|
||||
document.head.appendChild(script);
|
||||
document.head.appendChild(script);
|
||||
|
||||
const backBtn = document.getElementById('back-to-grid');
|
||||
const urlRevoker = () => {
|
||||
URL.revokeObjectURL(fileURL);
|
||||
state.currentPdfUrl = null;
|
||||
backBtn.removeEventListener('click', urlRevoker);
|
||||
};
|
||||
backBtn.addEventListener('click', urlRevoker);
|
||||
}
|
||||
};
|
||||
const backBtn = document.getElementById('back-to-grid');
|
||||
const urlRevoker = () => {
|
||||
URL.revokeObjectURL(fileURL);
|
||||
state.currentPdfUrl = null;
|
||||
backBtn.removeEventListener('click', urlRevoker);
|
||||
};
|
||||
backBtn.addEventListener('click', urlRevoker);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
|
||||
fileInput.addEventListener('change', (e) =>
|
||||
processFiles(Array.from((e.target as HTMLInputElement).files || []))
|
||||
);
|
||||
|
||||
const setupAddMoreButton = () => {
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
};
|
||||
const setupAddMoreButton = () => {
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
};
|
||||
|
||||
const setupClearButton = () => {
|
||||
const clearBtn = document.getElementById('clear-files-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
state.files = [];
|
||||
isFirstUpload = true;
|
||||
(fileInput as HTMLInputElement).value = '';
|
||||
const setupClearButton = () => {
|
||||
const clearBtn = document.getElementById('clear-files-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
state.files = [];
|
||||
isFirstUpload = true;
|
||||
(fileInput as HTMLInputElement).value = '';
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.textContent = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.textContent = '';
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
|
||||
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list', 'alternate-file-list'];
|
||||
toolSpecificUI.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = '';
|
||||
});
|
||||
const toolSpecificUI = [
|
||||
'file-list',
|
||||
'page-merge-preview',
|
||||
'image-list',
|
||||
'alternate-file-list',
|
||||
];
|
||||
toolSpecificUI.forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = '';
|
||||
});
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setupAddMoreButton();
|
||||
setupClearButton();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
setupAddMoreButton();
|
||||
setupClearButton();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -7,35 +7,35 @@ import { createIcons, icons } from 'lucide';
|
||||
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
|
||||
|
||||
export function setupToolInterface(toolId: any) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'instant' as ScrollBehavior
|
||||
});
|
||||
|
||||
state.activeTool = toolId;
|
||||
dom.toolContent.innerHTML = toolTemplates[toolId]();
|
||||
createIcons({icons});
|
||||
switchView('tool');
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'instant' as ScrollBehavior,
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
state.activeTool = toolId;
|
||||
dom.toolContent.innerHTML = toolTemplates[toolId]();
|
||||
createIcons({ icons });
|
||||
switchView('tool');
|
||||
|
||||
if (!fileInput && processBtn) {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileInput && processBtn) {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
setupFileInputHandler(toolId);
|
||||
}
|
||||
}
|
||||
if (fileInput) {
|
||||
setupFileInputHandler(toolId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,48 +4,53 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function addBlankPage() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageNumberInput = document.getElementById('page-number').value;
|
||||
if (pageNumberInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter a page number.');
|
||||
return;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageNumberInput = document.getElementById('page-number').value;
|
||||
if (pageNumberInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter a page number.');
|
||||
return;
|
||||
}
|
||||
|
||||
const position = parseInt(pageNumberInput);
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert(
|
||||
'Invalid Input',
|
||||
`Please enter a number between 0 and ${totalPages}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Adding page...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
|
||||
const indicesBefore = allIndices.slice(0, position);
|
||||
const indicesAfter = allIndices.slice(position);
|
||||
|
||||
if (indicesBefore.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
const position = parseInt(pageNumberInput);
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
||||
return;
|
||||
newPdf.addPage([width, height]);
|
||||
|
||||
if (indicesAfter.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
showLoader('Adding page...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
|
||||
const indicesBefore = allIndices.slice(0, position);
|
||||
const indicesAfter = allIndices.slice(position);
|
||||
|
||||
if (indicesBefore.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
newPdf.addPage([width, height]);
|
||||
|
||||
if (indicesAfter.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'page-added.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add a blank page.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'page-added.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add a blank page.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,98 +4,154 @@ import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
|
||||
export function setupHeaderFooterUI() {
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
if (totalPagesSpan && state.pdfDoc) {
|
||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
if (totalPagesSpan && state.pdfDoc) {
|
||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
}
|
||||
|
||||
export async function addHeaderFooter() {
|
||||
showLoader('Adding header & footer...');
|
||||
try {
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const allPages = state.pdfDoc.getPages();
|
||||
const totalPages = allPages.length;
|
||||
const margin = 40;
|
||||
showLoader('Adding header & footer...');
|
||||
try {
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const allPages = state.pdfDoc.getPages();
|
||||
const totalPages = allPages.length;
|
||||
const margin = 40;
|
||||
|
||||
// --- 1. Get new formatting options from the UI ---
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('font-color').value;
|
||||
const fontColor = hexToRgb(colorHex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
// --- 1. Get new formatting options from the UI ---
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('font-color').value;
|
||||
const fontColor = hexToRgb(colorHex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
|
||||
// --- 2. Get text values ---
|
||||
const texts = {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerLeft: document.getElementById('header-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerCenter: document.getElementById('header-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerRight: document.getElementById('header-right').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerLeft: document.getElementById('footer-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerCenter: document.getElementById('footer-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerRight: document.getElementById('footer-right').value,
|
||||
};
|
||||
// --- 2. Get text values ---
|
||||
const texts = {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerLeft: document.getElementById('header-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerCenter: document.getElementById('header-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerRight: document.getElementById('header-right').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerLeft: document.getElementById('footer-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerCenter: document.getElementById('footer-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerRight: document.getElementById('footer-right').value,
|
||||
};
|
||||
|
||||
// --- 3. Parse page range to determine which pages to modify ---
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0) {
|
||||
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
|
||||
}
|
||||
|
||||
// --- 4. Define drawing options with new values ---
|
||||
const drawOptions = {
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(fontColor.r, fontColor.g, fontColor.b)
|
||||
};
|
||||
|
||||
// --- 5. Loop over only the selected pages ---
|
||||
for (const pageIndex of indicesToProcess) {
|
||||
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
||||
const page = allPages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
const pageNumber = pageIndex + 1; // For dynamic text
|
||||
|
||||
// Helper to replace placeholders like {page} and {total}
|
||||
const processText = (text: any) => text
|
||||
.replace(/{page}/g, pageNumber)
|
||||
.replace(/{total}/g, totalPages);
|
||||
|
||||
// Get processed text for the current page
|
||||
const processedTexts = {
|
||||
headerLeft: processText(texts.headerLeft),
|
||||
headerCenter: processText(texts.headerCenter),
|
||||
headerRight: processText(texts.headerRight),
|
||||
footerLeft: processText(texts.footerLeft),
|
||||
footerCenter: processText(texts.footerCenter),
|
||||
footerRight: processText(texts.footerRight),
|
||||
};
|
||||
|
||||
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
||||
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
|
||||
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
|
||||
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
|
||||
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
|
||||
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
// --- 3. Parse page range to determine which pages to modify ---
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0) {
|
||||
throw new Error(
|
||||
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Define drawing options with new values ---
|
||||
const drawOptions = {
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(fontColor.r, fontColor.g, fontColor.b),
|
||||
};
|
||||
|
||||
// --- 5. Loop over only the selected pages ---
|
||||
for (const pageIndex of indicesToProcess) {
|
||||
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
||||
const page = allPages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
const pageNumber = pageIndex + 1; // For dynamic text
|
||||
|
||||
// Helper to replace placeholders like {page} and {total}
|
||||
const processText = (text: any) =>
|
||||
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
|
||||
|
||||
// Get processed text for the current page
|
||||
const processedTexts = {
|
||||
headerLeft: processText(texts.headerLeft),
|
||||
headerCenter: processText(texts.headerCenter),
|
||||
headerRight: processText(texts.headerRight),
|
||||
footerLeft: processText(texts.footerLeft),
|
||||
footerCenter: processText(texts.footerCenter),
|
||||
footerRight: processText(texts.footerRight),
|
||||
};
|
||||
|
||||
if (processedTexts.headerLeft)
|
||||
page.drawText(processedTexts.headerLeft, {
|
||||
...drawOptions,
|
||||
x: margin,
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.headerCenter)
|
||||
page.drawText(processedTexts.headerCenter, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width / 2 -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.headerCenter,
|
||||
fontSize
|
||||
) /
|
||||
2,
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.headerRight)
|
||||
page.drawText(processedTexts.headerRight, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width -
|
||||
margin -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.headerRight,
|
||||
fontSize
|
||||
),
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.footerLeft)
|
||||
page.drawText(processedTexts.footerLeft, {
|
||||
...drawOptions,
|
||||
x: margin,
|
||||
y: margin,
|
||||
});
|
||||
if (processedTexts.footerCenter)
|
||||
page.drawText(processedTexts.footerCenter, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width / 2 -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.footerCenter,
|
||||
fontSize
|
||||
) /
|
||||
2,
|
||||
y: margin,
|
||||
});
|
||||
if (processedTexts.footerRight)
|
||||
page.drawText(processedTexts.footerRight, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width -
|
||||
margin -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.footerRight,
|
||||
fontSize
|
||||
),
|
||||
y: margin,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'header-footer-added.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,97 +5,133 @@ import { state } from '../state.js';
|
||||
import { rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export async function addPageNumbers() {
|
||||
showLoader('Adding page numbers...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const position = document.getElementById('position').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const format = document.getElementById('number-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
showLoader('Adding page numbers...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const position = document.getElementById('position').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const format = document.getElementById('number-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const totalPages = pages.length;
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const totalPages = pages.length;
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
const mediaBox = page.getMediaBox();
|
||||
const cropBox = page.getCropBox();
|
||||
const bounds = cropBox || mediaBox;
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const xOffset = bounds.x || 0;
|
||||
const yOffset = bounds.y || 0;
|
||||
|
||||
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||
const textHeight = fontSize;
|
||||
|
||||
const minMargin = 8;
|
||||
const maxMargin = 40;
|
||||
const marginPercentage = 0.04;
|
||||
|
||||
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
|
||||
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
|
||||
|
||||
// Ensure text doesn't go outside visible page boundaries
|
||||
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
|
||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||
|
||||
let x, y;
|
||||
const mediaBox = page.getMediaBox();
|
||||
const cropBox = page.getCropBox();
|
||||
const bounds = cropBox || mediaBox;
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const xOffset = bounds.x || 0;
|
||||
const yOffset = bounds.y || 0;
|
||||
|
||||
switch (position) {
|
||||
case 'bottom-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'top-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
}
|
||||
let pageNumText =
|
||||
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||
|
||||
// Final safety check to ensure coordinates are within visible page bounds
|
||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||
const textHeight = fontSize;
|
||||
|
||||
page.drawText(pageNumText, {
|
||||
x, y,
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b)
|
||||
});
|
||||
}
|
||||
const minMargin = 8;
|
||||
const maxMargin = 40;
|
||||
const marginPercentage = 0.04;
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
|
||||
showAlert('Success', 'Page numbers added successfully!');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add page numbers.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const horizontalMargin = Math.max(
|
||||
minMargin,
|
||||
Math.min(maxMargin, width * marginPercentage)
|
||||
);
|
||||
const verticalMargin = Math.max(
|
||||
minMargin,
|
||||
Math.min(maxMargin, height * marginPercentage)
|
||||
);
|
||||
|
||||
// Ensure text doesn't go outside visible page boundaries
|
||||
const safeHorizontalMargin = Math.max(
|
||||
horizontalMargin,
|
||||
textWidth / 2 + 3
|
||||
);
|
||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||
|
||||
let x, y;
|
||||
|
||||
switch (position) {
|
||||
case 'bottom-center':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
Math.min(
|
||||
width - safeHorizontalMargin - textWidth,
|
||||
(width - textWidth) / 2
|
||||
)
|
||||
) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
width - safeHorizontalMargin - textWidth
|
||||
) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'top-center':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
Math.min(
|
||||
width - safeHorizontalMargin - textWidth,
|
||||
(width - textWidth) / 2
|
||||
)
|
||||
) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-right':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
width - safeHorizontalMargin - textWidth
|
||||
) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
// Final safety check to ensure coordinates are within visible page bounds
|
||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||
|
||||
page.drawText(pageNumText, {
|
||||
x,
|
||||
y,
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'paginated.pdf'
|
||||
);
|
||||
showAlert('Success', 'Page numbers added successfully!');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add page numbers.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +1,172 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
hexToRgb,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
rgb,
|
||||
degrees,
|
||||
StandardFonts,
|
||||
} from 'pdf-lib';
|
||||
|
||||
export function setupWatermarkUI() {
|
||||
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
const watermarkTypeRadios = document.querySelectorAll(
|
||||
'input[name="watermark-type"]'
|
||||
);
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
watermarkTypeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
if (e.target.value === 'text') {
|
||||
textOptions.classList.remove('hidden');
|
||||
imageOptions.classList.add('hidden');
|
||||
} else {
|
||||
textOptions.classList.add('hidden');
|
||||
imageOptions.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
watermarkTypeRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
if (e.target.value === 'text') {
|
||||
textOptions.classList.remove('hidden');
|
||||
imageOptions.classList.add('hidden');
|
||||
} else {
|
||||
textOptions.classList.add('hidden');
|
||||
imageOptions.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const opacitySliderText = document.getElementById('opacity-text');
|
||||
const opacityValueText = document.getElementById('opacity-value-text');
|
||||
const angleSliderText = document.getElementById('angle-text');
|
||||
const angleValueText = document.getElementById('angle-value-text');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderText.addEventListener('input', () => opacityValueText.textContent = opacitySliderText.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderText.addEventListener('input', () => angleValueText.textContent = angleSliderText.value);
|
||||
const opacitySliderText = document.getElementById('opacity-text');
|
||||
const opacityValueText = document.getElementById('opacity-value-text');
|
||||
const angleSliderText = document.getElementById('angle-text');
|
||||
const angleValueText = document.getElementById('angle-value-text');
|
||||
|
||||
const opacitySliderImage = document.getElementById('opacity-image');
|
||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||
const angleSliderImage = document.getElementById('angle-image');
|
||||
const angleValueImage = document.getElementById('angle-value-image');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderText.addEventListener(
|
||||
'input',
|
||||
() => (opacityValueText.textContent = opacitySliderText.value)
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderText.addEventListener(
|
||||
'input',
|
||||
() => (angleValueText.textContent = angleSliderText.value)
|
||||
);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
|
||||
const opacitySliderImage = document.getElementById('opacity-image');
|
||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||
const angleSliderImage = document.getElementById('angle-image');
|
||||
const angleValueImage = document.getElementById('angle-value-image');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderImage.addEventListener(
|
||||
'input',
|
||||
() => (opacityValueImage.textContent = opacitySliderImage.value)
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderImage.addEventListener(
|
||||
'input',
|
||||
() => (angleValueImage.textContent = angleSliderImage.value)
|
||||
);
|
||||
}
|
||||
|
||||
export async function addWatermark() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
|
||||
|
||||
showLoader('Adding watermark...');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const watermarkType = document.querySelector(
|
||||
'input[name="watermark-type"]:checked'
|
||||
).value;
|
||||
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
let watermarkAsset = null;
|
||||
showLoader('Adding watermark...');
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else { // 'image'
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageFile = document.getElementById('image-watermark-input').files[0];
|
||||
if (!imageFile) throw new Error('Please select an image file for the watermark.');
|
||||
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
if (imageFile.type === 'image/png') {
|
||||
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
||||
} else if (imageFile.type === 'image/jpeg') {
|
||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||
} else {
|
||||
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
|
||||
}
|
||||
}
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
let watermarkAsset = null;
|
||||
|
||||
for (const page of pages) {
|
||||
const { width, height } = page.getSize();
|
||||
if (watermarkType === 'text') {
|
||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else {
|
||||
// 'image'
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageFile = document.getElementById('image-watermark-input')
|
||||
.files[0];
|
||||
if (!imageFile)
|
||||
throw new Error('Please select an image file for the watermark.');
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('watermark-text').value;
|
||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 72;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle = parseInt(document.getElementById('angle-text').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||
|
||||
page.drawText(text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font: watermarkAsset,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle = parseInt(document.getElementById('angle-image').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
||||
|
||||
const scale = 0.5;
|
||||
const imgWidth = watermarkAsset.width * scale;
|
||||
const imgHeight = watermarkAsset.height * scale;
|
||||
|
||||
page.drawImage(watermarkAsset, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add the watermark. Please check your inputs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
if (imageFile.type === 'image/png') {
|
||||
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
||||
} else if (imageFile.type === 'image/jpeg') {
|
||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('watermark-text').value;
|
||||
if (!text.trim())
|
||||
throw new Error('Please enter text for the watermark.');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize =
|
||||
parseInt(document.getElementById('font-size').value) || 72;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle =
|
||||
parseInt(document.getElementById('angle-text').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const opacity =
|
||||
parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||
|
||||
page.drawText(text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font: watermarkAsset,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle =
|
||||
parseInt(document.getElementById('angle-image').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const opacity =
|
||||
parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
||||
|
||||
const scale = 0.5;
|
||||
const imgWidth = watermarkAsset.width * scale;
|
||||
const imgHeight = watermarkAsset.height * scale;
|
||||
|
||||
page.drawImage(watermarkAsset, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'watermarked.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
e.message || 'Could not add the watermark. Please check your inputs.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,103 +5,121 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const alternateMergeState = {
|
||||
pdfDocs: {},
|
||||
pdfDocs: {},
|
||||
};
|
||||
|
||||
export async function setupAlternateMergeTool() {
|
||||
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
|
||||
if (!optionsDiv || !processBtn || !fileList) return;
|
||||
if (!optionsDiv || !processBtn || !fileList) return;
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
processBtn.disabled = false;
|
||||
processBtn.onclick = alternateMerge;
|
||||
|
||||
fileList.innerHTML = '';
|
||||
alternateMergeState.pdfDocs = {};
|
||||
optionsDiv.classList.remove('hidden');
|
||||
processBtn.disabled = false;
|
||||
processBtn.onclick = alternateMerge;
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
||||
fileList.innerHTML = '';
|
||||
alternateMergeState.pdfDocs = {};
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||
li.dataset.fileName = file.name;
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex items-center gap-2 truncate';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const pagesSpan = document.createElement('span');
|
||||
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||
pagesSpan.textContent = `(${pageCount} pages)`;
|
||||
|
||||
infoDiv.append(nameSpan, pagesSpan);
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||
|
||||
li.append(infoDiv, dragHandle);
|
||||
fileList.appendChild(li);
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
|
||||
pdfBytes as ArrayBuffer,
|
||||
{
|
||||
ignoreEncryption: true,
|
||||
}
|
||||
);
|
||||
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
||||
|
||||
Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
});
|
||||
const li = document.createElement('li');
|
||||
li.className =
|
||||
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||
li.dataset.fileName = file.name;
|
||||
|
||||
} catch (error) {
|
||||
showAlert('Error', 'Failed to load one or more PDF files. They may be corrupted or password-protected.');
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoader();
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex items-center gap-2 truncate';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const pagesSpan = document.createElement('span');
|
||||
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||
pagesSpan.textContent = `(${pageCount} pages)`;
|
||||
|
||||
infoDiv.append(nameSpan, pagesSpan);
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className =
|
||||
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||
|
||||
li.append(infoDiv, dragHandle);
|
||||
fileList.appendChild(li);
|
||||
}
|
||||
|
||||
Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
});
|
||||
} catch (error) {
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
|
||||
);
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function alternateMerge() {
|
||||
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
||||
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
|
||||
return;
|
||||
}
|
||||
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
||||
showAlert(
|
||||
'Not Enough Files',
|
||||
'Please upload at least two PDF files to alternate and mix.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Alternating and mixing pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
const sortedFileNames = Array.from(fileList.children).map(li => (li as HTMLElement).dataset.fileName);
|
||||
showLoader('Alternating and mixing pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
const sortedFileNames = Array.from(fileList.children).map(
|
||||
(li) => (li as HTMLElement).dataset.fileName
|
||||
);
|
||||
|
||||
const loadedDocs = sortedFileNames.map(name => alternateMergeState.pdfDocs[name]);
|
||||
const pageCounts = loadedDocs.map(doc => doc.getPageCount());
|
||||
const maxPages = Math.max(...pageCounts);
|
||||
const loadedDocs = sortedFileNames.map(
|
||||
(name) => alternateMergeState.pdfDocs[name]
|
||||
);
|
||||
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
|
||||
const maxPages = Math.max(...pageCounts);
|
||||
|
||||
for (let i = 0; i < maxPages; i++) {
|
||||
for (const doc of loadedDocs) {
|
||||
if (i < doc.getPageCount()) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < maxPages; i++) {
|
||||
for (const doc of loadedDocs) {
|
||||
if (i < doc.getPageCount()) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'alternated-mixed.pdf');
|
||||
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||
|
||||
} catch (e) {
|
||||
console.error('Alternate Merge error:', e);
|
||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||
'alternated-mixed.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||
} catch (e) {
|
||||
console.error('Alternate Merge error:', e);
|
||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,51 +5,64 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
async function convertImageToPngBytes(file: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function bmpToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_bmps.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,48 +5,51 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function changeBackgroundColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('background-color').value;
|
||||
const color = hexToRgb(colorHex);
|
||||
|
||||
showLoader('Changing background color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
const newPage = newPdfDoc.addPage([width, height]);
|
||||
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
});
|
||||
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('background-color').value;
|
||||
const color = hexToRgb(colorHex);
|
||||
|
||||
showLoader('Changing background color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
const newPage = newPdfDoc.addPage([width, height]);
|
||||
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
});
|
||||
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change the background color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'background-changed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change the background color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,146 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function changePermissions() {
|
||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
|
||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
|
||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
|
||||
|
||||
// An owner password is required to enforce any permissions.
|
||||
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
|
||||
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing to process...');
|
||||
|
||||
const currentPassword = (
|
||||
document.getElementById('current-password') as HTMLInputElement
|
||||
).value;
|
||||
const newUserPassword = (
|
||||
document.getElementById('new-user-password') as HTMLInputElement
|
||||
).value;
|
||||
const newOwnerPassword = (
|
||||
document.getElementById('new-owner-password') as HTMLInputElement
|
||||
).value;
|
||||
|
||||
// An owner password is required to enforce any permissions.
|
||||
if (
|
||||
!newOwnerPassword &&
|
||||
(newUserPassword ||
|
||||
document.querySelectorAll('input[type="checkbox"]:not(:checked)').length >
|
||||
0)
|
||||
) {
|
||||
showAlert(
|
||||
'Input Required',
|
||||
'You must set a "New Owner Password" to enforce specific permissions or to set a user password.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing to process...');
|
||||
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
|
||||
let pdf;
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
|
||||
let pdf;
|
||||
try {
|
||||
pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData as ArrayBuffer,
|
||||
password: currentPassword
|
||||
}).promise;
|
||||
} catch (e) {
|
||||
// This catch is specific to password errors in pdf.js
|
||||
if (e.name === 'PasswordException') {
|
||||
hideLoader();
|
||||
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Applying new permissions...';
|
||||
|
||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
|
||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement).checked;
|
||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement).checked;
|
||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement).checked;
|
||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement).checked;
|
||||
const allowContentAccessibility = (document.getElementById('allow-content-accessibility') as HTMLInputElement).checked;
|
||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement).checked;
|
||||
|
||||
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
||||
|
||||
// Apply the new, separate user and owner passwords
|
||||
userPassword: newUserPassword,
|
||||
ownerPassword: newOwnerPassword,
|
||||
|
||||
// Apply all seven permissions from the checkboxes
|
||||
permissions: {
|
||||
printing: allowPrinting ? 'highResolution' : false,
|
||||
modifying: allowModifying,
|
||||
copying: allowCopying,
|
||||
annotating: allowAnnotating,
|
||||
fillingForms: allowFillingForms,
|
||||
contentAccessibility: allowContentAccessibility,
|
||||
documentAssembly: allowDocumentAssembly
|
||||
}
|
||||
});
|
||||
|
||||
const stream = doc.pipe(blobStream());
|
||||
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height
|
||||
});
|
||||
}
|
||||
|
||||
doc.end();
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Permissions changed successfully!');
|
||||
});
|
||||
|
||||
pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData as ArrayBuffer,
|
||||
password: currentPassword,
|
||||
}).promise;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// This catch is specific to password errors in pdf.js
|
||||
if (e.name === 'PasswordException') {
|
||||
hideLoader();
|
||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Applying new permissions...';
|
||||
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
).checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
).checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
).checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
).checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
).checked;
|
||||
const allowContentAccessibility = (
|
||||
document.getElementById('allow-content-accessibility') as HTMLInputElement
|
||||
).checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
).checked;
|
||||
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
||||
|
||||
// Apply the new, separate user and owner passwords
|
||||
userPassword: newUserPassword,
|
||||
ownerPassword: newOwnerPassword,
|
||||
|
||||
// Apply all seven permissions from the checkboxes
|
||||
permissions: {
|
||||
printing: allowPrinting ? 'highResolution' : false,
|
||||
modifying: allowModifying,
|
||||
copying: allowCopying,
|
||||
annotating: allowAnnotating,
|
||||
fillingForms: allowFillingForms,
|
||||
contentAccessibility: allowContentAccessibility,
|
||||
documentAssembly: allowDocumentAssembly,
|
||||
},
|
||||
});
|
||||
|
||||
const stream = doc.pipe(blobStream());
|
||||
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
}
|
||||
|
||||
doc.end();
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Permissions changed successfully!');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
hexToRgb,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
@@ -9,136 +12,168 @@ let isRenderingPreview = false;
|
||||
let renderTimeout: any;
|
||||
|
||||
async function updateTextColorPreview() {
|
||||
if (isRenderingPreview) return;
|
||||
isRenderingPreview = true;
|
||||
if (isRenderingPreview) return;
|
||||
isRenderingPreview = true;
|
||||
|
||||
try {
|
||||
const textColorCanvas = document.getElementById('text-color-canvas');
|
||||
if (!textColorCanvas) return;
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const page = await pdf.getPage(1); // Preview first page
|
||||
const viewport = page.getViewport({ scale: 0.8 });
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||
const context = textColorCanvas.getContext('2d');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
textColorCanvas.width = viewport.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||
textColorCanvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
|
||||
const data = imageData.data;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color-input').value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
|
||||
data[i] = r * 255;
|
||||
data[i + 1] = g * 255;
|
||||
data[i + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
} finally {
|
||||
isRenderingPreview = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupTextColorTool() {
|
||||
const originalCanvas = document.getElementById('original-canvas');
|
||||
const colorInput = document.getElementById('text-color-input');
|
||||
|
||||
if (!originalCanvas || !colorInput) return;
|
||||
|
||||
// Debounce the preview update for performance
|
||||
colorInput.addEventListener('input', () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||
});
|
||||
try {
|
||||
const textColorCanvas = document.getElementById('text-color-canvas');
|
||||
if (!textColorCanvas) return;
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const page = await pdf.getPage(1); // Preview first page
|
||||
const viewport = page.getViewport({ scale: 0.8 });
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
originalCanvas.width = viewport.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||
originalCanvas.height = viewport.height;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
|
||||
await updateTextColorPreview();
|
||||
}
|
||||
const context = textColorCanvas.getContext('2d');
|
||||
|
||||
export async function changeTextColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
textColorCanvas.width = viewport.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||
textColorCanvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageData = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
textColorCanvas.width,
|
||||
textColorCanvas.height
|
||||
);
|
||||
const data = imageData.data;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color-input').value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
showLoader('Changing text color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
||||
data[j] = r * 255;
|
||||
data[j + 1] = g * 255;
|
||||
data[j + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change text color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (
|
||||
data[i] < darknessThreshold &&
|
||||
data[i + 1] < darknessThreshold &&
|
||||
data[i + 2] < darknessThreshold
|
||||
) {
|
||||
data[i] = r * 255;
|
||||
data[i + 1] = g * 255;
|
||||
data[i + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
} finally {
|
||||
isRenderingPreview = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupTextColorTool() {
|
||||
const originalCanvas = document.getElementById('original-canvas');
|
||||
const colorInput = document.getElementById('text-color-input');
|
||||
|
||||
if (!originalCanvas || !colorInput) return;
|
||||
|
||||
// Debounce the preview update for performance
|
||||
colorInput.addEventListener('input', () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||
});
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 0.8 });
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
originalCanvas.width = viewport.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||
originalCanvas.height = viewport.height;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||
await page.render({
|
||||
canvasContext: originalCanvas.getContext('2d'),
|
||||
viewport,
|
||||
}).promise;
|
||||
await updateTextColorPreview();
|
||||
}
|
||||
|
||||
export async function changeTextColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color-input').value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
showLoader('Changing text color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
if (
|
||||
data[j] < darknessThreshold &&
|
||||
data[j + 1] < darknessThreshold &&
|
||||
data[j + 2] < darknessThreshold
|
||||
) {
|
||||
data[j] = r * 255;
|
||||
data[j + 1] = g * 255;
|
||||
data[j + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'text-color-changed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change text color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,72 +5,74 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function combineToSinglePage() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColorHex = document.getElementById('background-color').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addSeparator = document.getElementById('add-separator').checked;
|
||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColorHex = document.getElementById('background-color').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addSeparator = document.getElementById('add-separator').checked;
|
||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||
|
||||
showLoader('Combining pages...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
showLoader('Combining pages...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
let maxWidth = 0;
|
||||
let totalHeight = 0;
|
||||
sourcePages.forEach((page: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
totalHeight += height;
|
||||
});
|
||||
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
|
||||
let maxWidth = 0;
|
||||
let totalHeight = 0;
|
||||
sourcePages.forEach((page: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
totalHeight += height;
|
||||
});
|
||||
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
|
||||
|
||||
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
||||
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
||||
|
||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: maxWidth,
|
||||
height: totalHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
let currentY = totalHeight;
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
currentY -= height;
|
||||
const x = (maxWidth - width) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||
|
||||
if (addSeparator && i < sourcePages.length - 1) {
|
||||
const lineY = currentY - (spacing / 2);
|
||||
newPage.drawLine({
|
||||
start: { x: 0, y: lineY },
|
||||
end: { x: maxWidth, y: lineY },
|
||||
thickness: 0.5,
|
||||
color: rgb(0.8, 0.8, 0.8),
|
||||
});
|
||||
}
|
||||
|
||||
currentY -= spacing;
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'combined-page.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while combining pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: maxWidth,
|
||||
height: totalHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let currentY = totalHeight;
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
currentY -= height;
|
||||
const x = (maxWidth - width) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||
|
||||
if (addSeparator && i < sourcePages.length - 1) {
|
||||
const lineY = currentY - spacing / 2;
|
||||
newPage.drawLine({
|
||||
start: { x: 0, y: lineY },
|
||||
end: { x: maxWidth, y: lineY },
|
||||
thickness: 0.5,
|
||||
color: rgb(0.8, 0.8, 0.8),
|
||||
});
|
||||
}
|
||||
|
||||
currentY -= spacing;
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'combined-page.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while combining pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from "lucide";
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
const state = {
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
currentPage: 1,
|
||||
viewMode: 'overlay',
|
||||
isSyncScroll: true,
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
currentPage: 1,
|
||||
viewMode: 'overlay',
|
||||
isSyncScroll: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -17,110 +17,137 @@ const state = {
|
||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
||||
* @param {HTMLElement} container - The container to fit the canvas into.
|
||||
*/
|
||||
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
async function renderPage(
|
||||
pdfDoc: any,
|
||||
pageNum: any,
|
||||
canvas: any,
|
||||
container: any
|
||||
) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
|
||||
// Calculate scale to fit the container width.
|
||||
const containerWidth = container.clientWidth - 2; // Subtract border width
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale: scale });
|
||||
// Calculate scale to fit the container width.
|
||||
const containerWidth = container.clientWidth - 2; // Subtract border width
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale: scale });
|
||||
|
||||
canvas.width = scaledViewport.width;
|
||||
canvas.height = scaledViewport.height;
|
||||
canvas.width = scaledViewport.width;
|
||||
canvas.height = scaledViewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: scaledViewport,
|
||||
}).promise;
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: scaledViewport,
|
||||
}).promise;
|
||||
}
|
||||
|
||||
|
||||
async function renderBothPages() {
|
||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||
|
||||
showLoader(`Loading page ${state.currentPage}...`);
|
||||
showLoader(`Loading page ${state.currentPage}...`);
|
||||
|
||||
const canvas1 = document.getElementById('canvas-compare-1');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const canvas1 = document.getElementById('canvas-compare-1');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
|
||||
// Determine the correct container based on the view mode
|
||||
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||
// Determine the correct container based on the view mode
|
||||
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||
|
||||
await Promise.all([
|
||||
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
|
||||
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
|
||||
]);
|
||||
await Promise.all([
|
||||
renderPage(
|
||||
state.pdfDoc1,
|
||||
Math.min(state.currentPage, state.pdfDoc1.numPages),
|
||||
canvas1,
|
||||
container1
|
||||
),
|
||||
renderPage(
|
||||
state.pdfDoc2,
|
||||
Math.min(state.currentPage, state.pdfDoc2.numPages),
|
||||
canvas2,
|
||||
container2
|
||||
),
|
||||
]);
|
||||
|
||||
updateNavControls();
|
||||
hideLoader();
|
||||
updateNavControls();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function updateNavControls() {
|
||||
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('current-page-display-compare').textContent = state.currentPage;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page-compare').disabled = state.currentPage >= maxPages;
|
||||
const maxPages = Math.max(
|
||||
state.pdfDoc1?.numPages || 0,
|
||||
state.pdfDoc2?.numPages || 0
|
||||
);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('current-page-display-compare').textContent =
|
||||
state.currentPage;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page-compare').disabled =
|
||||
state.currentPage <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page-compare').disabled =
|
||||
state.currentPage >= maxPages;
|
||||
}
|
||||
|
||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||
|
||||
const handleFile = async (file: any) => {
|
||||
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||
const handleFile = async (file: any) => {
|
||||
if (!file || file.type !== 'application/pdf')
|
||||
return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||
|
||||
const displayDiv = document.getElementById(displayId);
|
||||
displayDiv.textContent = '';
|
||||
const displayDiv = document.getElementById(displayId);
|
||||
displayDiv.textContent = '';
|
||||
|
||||
// 2. Create the icon element
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check-circle');
|
||||
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||
// 2. Create the icon element
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check-circle');
|
||||
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||
|
||||
// 3. Create the paragraph element for the file name
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-300 truncate';
|
||||
// 3. Create the paragraph element for the file name
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-300 truncate';
|
||||
|
||||
// 4. Set the file name safely using textContent
|
||||
p.textContent = file.name;
|
||||
// 4. Set the file name safely using textContent
|
||||
p.textContent = file.name;
|
||||
|
||||
// 5. Append the safe elements to the container
|
||||
displayDiv.append(icon, p);
|
||||
createIcons({ icons });
|
||||
// 5. Append the safe elements to the container
|
||||
displayDiv.append(icon, p);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader(`Loading ${file.name}...`);
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
|
||||
try {
|
||||
showLoader(`Loading ${file.name}...`);
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
|
||||
|
||||
if (state.pdfDoc1 && state.pdfDoc2) {
|
||||
document.getElementById('compare-viewer').classList.remove('hidden');
|
||||
state.currentPage = 1;
|
||||
await renderBothPages();
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
||||
console.error(e);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
};
|
||||
if (state.pdfDoc1 && state.pdfDoc2) {
|
||||
document.getElementById('compare-viewer').classList.remove('hidden');
|
||||
state.currentPage = 1;
|
||||
await renderBothPages();
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not load PDF. It may be corrupt or password-protected.'
|
||||
);
|
||||
console.error(e);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,81 +155,92 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||
* @param {'overlay' | 'side-by-side'} mode
|
||||
*/
|
||||
function setViewMode(mode: any) {
|
||||
state.viewMode = mode;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const overlayControls = document.getElementById('overlay-controls');
|
||||
const sideControls = document.getElementById('side-by-side-controls');
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const opacitySlider = document.getElementById('opacity-slider');
|
||||
state.viewMode = mode;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const overlayControls = document.getElementById('overlay-controls');
|
||||
const sideControls = document.getElementById('side-by-side-controls');
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const opacitySlider = document.getElementById('opacity-slider');
|
||||
|
||||
|
||||
if (mode === 'overlay') {
|
||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||
overlayControls.classList.remove('hidden');
|
||||
sideControls.classList.add('hidden');
|
||||
btnOverlay.classList.add('bg-indigo-600');
|
||||
btnSide.classList.remove('bg-indigo-600');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = opacitySlider.value;
|
||||
} else {
|
||||
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
||||
overlayControls.classList.add('hidden');
|
||||
sideControls.classList.remove('hidden');
|
||||
btnOverlay.classList.remove('bg-indigo-600');
|
||||
btnSide.classList.add('bg-indigo-600');
|
||||
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
||||
canvas2.style.opacity = '1';
|
||||
}
|
||||
renderBothPages();
|
||||
if (mode === 'overlay') {
|
||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||
overlayControls.classList.remove('hidden');
|
||||
sideControls.classList.add('hidden');
|
||||
btnOverlay.classList.add('bg-indigo-600');
|
||||
btnSide.classList.remove('bg-indigo-600');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = opacitySlider.value;
|
||||
} else {
|
||||
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
||||
overlayControls.classList.add('hidden');
|
||||
sideControls.classList.remove('hidden');
|
||||
btnOverlay.classList.remove('bg-indigo-600');
|
||||
btnSide.classList.add('bg-indigo-600');
|
||||
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
||||
canvas2.style.opacity = '1';
|
||||
}
|
||||
renderBothPages();
|
||||
}
|
||||
|
||||
export function setupCompareTool() {
|
||||
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||
|
||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
|
||||
});
|
||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
|
||||
});
|
||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||
if (state.currentPage > 1) {
|
||||
state.currentPage--;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||
if (state.currentPage < maxPages) {
|
||||
state.currentPage++;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
|
||||
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
|
||||
document
|
||||
.getElementById('view-mode-overlay')
|
||||
.addEventListener('click', () => setViewMode('overlay'));
|
||||
document
|
||||
.getElementById('view-mode-side')
|
||||
.addEventListener('click', () => setViewMode('side-by-side'));
|
||||
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
|
||||
});
|
||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||
canvas2.style.transition = '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = e.target.value;
|
||||
});
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
|
||||
});
|
||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||
canvas2.style.transition = '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = e.target.value;
|
||||
});
|
||||
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
syncToggle.addEventListener('change', () => {
|
||||
state.isSyncScroll = syncToggle.checked;
|
||||
});
|
||||
|
||||
let scrollingPanel: any = null;
|
||||
panel1.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||
scrollingPanel = panel1;
|
||||
panel2.scrollTop = panel1.scrollTop;
|
||||
setTimeout(() => scrollingPanel = null, 100);
|
||||
}
|
||||
});
|
||||
panel2.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||
scrollingPanel = panel2;
|
||||
panel1.scrollTop = panel2.scrollTop;
|
||||
setTimeout(() => scrollingPanel = null, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
let scrollingPanel: any = null;
|
||||
panel1.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||
scrollingPanel = panel1;
|
||||
panel2.scrollTop = panel1.scrollTop;
|
||||
setTimeout(() => (scrollingPanel = null), 100);
|
||||
}
|
||||
});
|
||||
panel2.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||
scrollingPanel = panel2;
|
||||
panel1.scrollTop = panel2.scrollTop;
|
||||
setTimeout(() => (scrollingPanel = null), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,279 +1,352 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
|
||||
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
||||
|
||||
function dataUrlToBytes(dataUrl: any) {
|
||||
const base64 = dataUrl.split(',')[1];
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
const base64 = dataUrl.split(',')[1];
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
||||
const pages = pdfDoc.getPages();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
if (settings.removeMetadata) {
|
||||
try {
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
} catch (e) {
|
||||
console.warn('Could not remove metadata:', e);
|
||||
}
|
||||
if (settings.removeMetadata) {
|
||||
try {
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
} catch (e) {
|
||||
console.warn('Could not remove metadata:', e);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const resources = page.node.Resources();
|
||||
if (!resources) continue;
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const resources = page.node.Resources();
|
||||
if (!resources) continue;
|
||||
|
||||
const xobjects = resources.lookup(PDFName.of('XObject'));
|
||||
if (!(xobjects instanceof PDFDict)) continue;
|
||||
const xobjects = resources.lookup(PDFName.of('XObject'));
|
||||
if (!(xobjects instanceof PDFDict)) continue;
|
||||
|
||||
for (const [key, value] of xobjects.entries()) {
|
||||
const stream = pdfDoc.context.lookup(value);
|
||||
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
|
||||
for (const [key, value] of xobjects.entries()) {
|
||||
const stream = pdfDoc.context.lookup(value);
|
||||
if (
|
||||
!(stream instanceof PDFStream) ||
|
||||
stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')
|
||||
)
|
||||
continue;
|
||||
|
||||
try {
|
||||
const imageBytes = stream.getContents();
|
||||
if (imageBytes.length < settings.skipSize) continue;
|
||||
try {
|
||||
const imageBytes = stream.getContents();
|
||||
if (imageBytes.length < settings.skipSize) continue;
|
||||
|
||||
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
|
||||
: 8;
|
||||
const width =
|
||||
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const height =
|
||||
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const bitsPerComponent =
|
||||
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||
? (
|
||||
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
||||
).asNumber()
|
||||
: 8;
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
let newWidth = width;
|
||||
let newHeight = height;
|
||||
if (width > 0 && height > 0) {
|
||||
let newWidth = width;
|
||||
let newHeight = height;
|
||||
|
||||
const scaleFactor = settings.scaleFactor || 1.0;
|
||||
newWidth = Math.floor(width * scaleFactor);
|
||||
newHeight = Math.floor(height * scaleFactor);
|
||||
const scaleFactor = settings.scaleFactor || 1.0;
|
||||
newWidth = Math.floor(width * scaleFactor);
|
||||
newHeight = Math.floor(height * scaleFactor);
|
||||
|
||||
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
|
||||
const aspectRatio = newWidth / newHeight;
|
||||
if (newWidth > newHeight) {
|
||||
newWidth = Math.min(newWidth, settings.maxWidth);
|
||||
newHeight = newWidth / aspectRatio;
|
||||
} else {
|
||||
newHeight = Math.min(newHeight, settings.maxHeight);
|
||||
newWidth = newHeight * aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const minDim = settings.minDimension || 50;
|
||||
if (newWidth < minDim || newHeight < minDim) continue;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = Math.floor(newWidth);
|
||||
canvas.height = Math.floor(newHeight);
|
||||
|
||||
const img = new Image();
|
||||
const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(imageBytes)]));
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = imageUrl;
|
||||
});
|
||||
|
||||
ctx.imageSmoothingEnabled = settings.smoothing !== false;
|
||||
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
|
||||
|
||||
if (settings.grayscale) {
|
||||
ctx.filter = 'grayscale(100%)';
|
||||
} else if (settings.contrast) {
|
||||
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
let bestBytes = null;
|
||||
let bestSize = imageBytes.length;
|
||||
|
||||
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
|
||||
const jpegBytes = dataUrlToBytes(jpegDataUrl);
|
||||
if (jpegBytes.length < bestSize) {
|
||||
bestBytes = jpegBytes;
|
||||
bestSize = jpegBytes.length;
|
||||
}
|
||||
|
||||
if (settings.tryWebP) {
|
||||
try {
|
||||
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
|
||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||
if (webpBytes.length < bestSize) {
|
||||
bestBytes = webpBytes;
|
||||
bestSize = webpBytes.length;
|
||||
}
|
||||
} catch (e) { /* WebP not supported */ }
|
||||
}
|
||||
|
||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||
(stream as any).contents = bestBytes;
|
||||
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
|
||||
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
|
||||
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
|
||||
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
|
||||
stream.dict.delete(PDFName.of('DecodeParms'));
|
||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||
|
||||
if (settings.grayscale) {
|
||||
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
|
||||
}
|
||||
}
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Skipping an uncompressible image in smart mode:', error);
|
||||
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
|
||||
const aspectRatio = newWidth / newHeight;
|
||||
if (newWidth > newHeight) {
|
||||
newWidth = Math.min(newWidth, settings.maxWidth);
|
||||
newHeight = newWidth / aspectRatio;
|
||||
} else {
|
||||
newHeight = Math.min(newHeight, settings.maxHeight);
|
||||
newWidth = newHeight * aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const minDim = settings.minDimension || 50;
|
||||
if (newWidth < minDim || newHeight < minDim) continue;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = Math.floor(newWidth);
|
||||
canvas.height = Math.floor(newHeight);
|
||||
|
||||
const img = new Image();
|
||||
const imageUrl = URL.createObjectURL(
|
||||
new Blob([new Uint8Array(imageBytes)])
|
||||
);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = imageUrl;
|
||||
});
|
||||
|
||||
ctx.imageSmoothingEnabled = settings.smoothing !== false;
|
||||
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
|
||||
|
||||
if (settings.grayscale) {
|
||||
ctx.filter = 'grayscale(100%)';
|
||||
} else if (settings.contrast) {
|
||||
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
let bestBytes = null;
|
||||
let bestSize = imageBytes.length;
|
||||
|
||||
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
|
||||
const jpegBytes = dataUrlToBytes(jpegDataUrl);
|
||||
if (jpegBytes.length < bestSize) {
|
||||
bestBytes = jpegBytes;
|
||||
bestSize = jpegBytes.length;
|
||||
}
|
||||
|
||||
if (settings.tryWebP) {
|
||||
try {
|
||||
const webpDataUrl = canvas.toDataURL(
|
||||
'image/webp',
|
||||
settings.quality
|
||||
);
|
||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||
if (webpBytes.length < bestSize) {
|
||||
bestBytes = webpBytes;
|
||||
bestSize = webpBytes.length;
|
||||
}
|
||||
} catch (e) {
|
||||
/* WebP not supported */
|
||||
}
|
||||
}
|
||||
|
||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||
(stream as any).contents = bestBytes;
|
||||
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
|
||||
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
|
||||
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
|
||||
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
|
||||
stream.dict.delete(PDFName.of('DecodeParms'));
|
||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||
|
||||
if (settings.grayscale) {
|
||||
stream.dict.set(
|
||||
PDFName.of('ColorSpace'),
|
||||
PDFName.of('DeviceGray')
|
||||
);
|
||||
}
|
||||
}
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Skipping an uncompressible image in smart mode:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveOptions = {
|
||||
useObjectStreams: settings.useObjectStreams !== false,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: settings.objectsPerTick || 50
|
||||
};
|
||||
const saveOptions = {
|
||||
useObjectStreams: settings.useObjectStreams !== false,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: settings.objectsPerTick || 50,
|
||||
};
|
||||
|
||||
return await pdfDoc.save(saveOptions);
|
||||
return await pdfDoc.save(saveOptions);
|
||||
}
|
||||
|
||||
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
||||
const page = await pdfJsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: settings.scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
||||
const page = await pdfJsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: settings.scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||
.promise;
|
||||
|
||||
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
}
|
||||
return await newPdfDoc.save();
|
||||
const jpegBlob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(jpegImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
return await newPdfDoc.save();
|
||||
}
|
||||
|
||||
export async function compress() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const level = document.getElementById('compression-level').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const algorithm = document.getElementById('compression-algorithm').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const level = document.getElementById('compression-level').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const algorithm = document.getElementById('compression-algorithm').value;
|
||||
|
||||
const settings = {
|
||||
'balanced': {
|
||||
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
|
||||
legacy: { scale: 1.5, quality: 0.6 }
|
||||
},
|
||||
'high-quality': {
|
||||
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
|
||||
legacy: { scale: 2.0, quality: 0.9 }
|
||||
},
|
||||
'small-size': {
|
||||
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
|
||||
legacy: { scale: 1.2, quality: 0.4 }
|
||||
},
|
||||
'extreme': {
|
||||
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
|
||||
legacy: { scale: 1.0, quality: 0.2 }
|
||||
}
|
||||
};
|
||||
const settings = {
|
||||
balanced: {
|
||||
smart: {
|
||||
quality: 0.5,
|
||||
threshold: 0.95,
|
||||
maxWidth: 1800,
|
||||
maxHeight: 1800,
|
||||
skipSize: 3000,
|
||||
},
|
||||
legacy: { scale: 1.5, quality: 0.6 },
|
||||
},
|
||||
'high-quality': {
|
||||
smart: {
|
||||
quality: 0.7,
|
||||
threshold: 0.98,
|
||||
maxWidth: 2500,
|
||||
maxHeight: 2500,
|
||||
skipSize: 5000,
|
||||
},
|
||||
legacy: { scale: 2.0, quality: 0.9 },
|
||||
},
|
||||
'small-size': {
|
||||
smart: {
|
||||
quality: 0.3,
|
||||
threshold: 0.95,
|
||||
maxWidth: 1200,
|
||||
maxHeight: 1200,
|
||||
skipSize: 2000,
|
||||
},
|
||||
legacy: { scale: 1.2, quality: 0.4 },
|
||||
},
|
||||
extreme: {
|
||||
smart: {
|
||||
quality: 0.1,
|
||||
threshold: 0.95,
|
||||
maxWidth: 1000,
|
||||
maxHeight: 1000,
|
||||
skipSize: 1000,
|
||||
},
|
||||
legacy: { scale: 1.0, quality: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||
const legacySettings = settings[level].legacy;
|
||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||
const legacySettings = settings[level].legacy;
|
||||
|
||||
try {
|
||||
const originalFile = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
||||
try {
|
||||
const originalFile = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
||||
|
||||
let resultBytes;
|
||||
let usedMethod;
|
||||
let resultBytes;
|
||||
let usedMethod;
|
||||
|
||||
if (algorithm === 'vector') {
|
||||
showLoader('Running Vector (Smart) compression...');
|
||||
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
usedMethod = 'Vector';
|
||||
} else if (algorithm === 'photon') {
|
||||
showLoader('Running Photon (Rasterize) compression...');
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
usedMethod = 'Photon';
|
||||
} else {
|
||||
showLoader('Running Automatic (Vector first)...');
|
||||
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
if (algorithm === 'vector') {
|
||||
showLoader('Running Vector (Smart) compression...');
|
||||
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
usedMethod = 'Vector';
|
||||
} else if (algorithm === 'photon') {
|
||||
showLoader('Running Photon (Rasterize) compression...');
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
usedMethod = 'Photon';
|
||||
} else {
|
||||
showLoader('Running Automatic (Vector first)...');
|
||||
const vectorResultBytes = await performSmartCompression(
|
||||
arrayBuffer,
|
||||
smartSettings
|
||||
);
|
||||
|
||||
if (vectorResultBytes.length < originalFile.size) {
|
||||
resultBytes = vectorResultBytes;
|
||||
usedMethod = 'Vector (Automatic)';
|
||||
} else {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
|
||||
showLoader('Running Automatic (Photon fallback)...');
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
usedMethod = 'Photon (Automatic)';
|
||||
}
|
||||
}
|
||||
|
||||
const originalSize = formatBytes(originalFile.size);
|
||||
const compressedSize = formatBytes(resultBytes.length);
|
||||
const savings = originalFile.size - resultBytes.length;
|
||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||
|
||||
if (savings > 0) {
|
||||
showAlert(
|
||||
'Compression Complete',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Compression Finished',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (vectorResultBytes.length < originalFile.size) {
|
||||
resultBytes = vectorResultBytes;
|
||||
usedMethod = 'Vector (Automatic)';
|
||||
} else {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
|
||||
} finally {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Vector failed to reduce size. Trying Photon...',
|
||||
'info',
|
||||
3000
|
||||
);
|
||||
showLoader('Running Automatic (Photon fallback)...');
|
||||
resultBytes = await performLegacyCompression(
|
||||
arrayBuffer,
|
||||
legacySettings
|
||||
);
|
||||
usedMethod = 'Photon (Automatic)';
|
||||
}
|
||||
}
|
||||
|
||||
const originalSize = formatBytes(originalFile.size);
|
||||
const compressedSize = formatBytes(resultBytes.length);
|
||||
const savings = originalFile.size - resultBytes.length;
|
||||
const savingsPercent =
|
||||
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||
|
||||
if (savings > 0) {
|
||||
showAlert(
|
||||
'Compression Complete',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Compression Finished',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
downloadFile(
|
||||
new Blob([resultBytes], { type: 'application/pdf' }),
|
||||
'compressed-final.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during compression. Error: ${e.message}`,
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Cropper from "cropperjs";
|
||||
import Cropper from 'cropperjs';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
|
||||
// --- Global State for the Cropper Tool ---
|
||||
const cropperState = {
|
||||
pdfDoc: null,
|
||||
currentPageNum: 1,
|
||||
cropper: null,
|
||||
originalPdfBytes: null,
|
||||
cropperImageElement: null,
|
||||
pageCrops: {},
|
||||
pdfDoc: null,
|
||||
currentPageNum: 1,
|
||||
cropper: null,
|
||||
originalPdfBytes: null,
|
||||
cropperImageElement: null,
|
||||
pageCrops: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the current crop data to the state object.
|
||||
*/
|
||||
function saveCurrentCrop() {
|
||||
if (cropperState.cropper) {
|
||||
const currentCrop = cropperState.cropper.getData(true);
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropPercentages = {
|
||||
x: currentCrop.x / imageData.naturalWidth,
|
||||
y: currentCrop.y / imageData.naturalHeight,
|
||||
width: currentCrop.width / imageData.naturalWidth,
|
||||
height: currentCrop.height / imageData.naturalHeight,
|
||||
};
|
||||
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
||||
}
|
||||
if (cropperState.cropper) {
|
||||
const currentCrop = cropperState.cropper.getData(true);
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropPercentages = {
|
||||
x: currentCrop.x / imageData.naturalWidth,
|
||||
y: currentCrop.y / imageData.naturalHeight,
|
||||
width: currentCrop.width / imageData.naturalWidth,
|
||||
height: currentCrop.height / imageData.naturalHeight,
|
||||
};
|
||||
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,60 +37,60 @@ function saveCurrentCrop() {
|
||||
* @param {number} num The page number to render.
|
||||
*/
|
||||
async function displayPageAsImage(num: any) {
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
if (cropperState.cropper) {
|
||||
cropperState.cropper.destroy();
|
||||
}
|
||||
|
||||
const image = document.createElement('img');
|
||||
image.src = tempCanvas.toDataURL('image/png');
|
||||
document.getElementById('cropper-container').innerHTML = '';
|
||||
document.getElementById('cropper-container').appendChild(image);
|
||||
|
||||
image.onload = () => {
|
||||
cropperState.cropper = new Cropper(image, {
|
||||
viewMode: 1,
|
||||
background: false,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
rotatable: false,
|
||||
zoomable: false,
|
||||
});
|
||||
|
||||
// Restore saved crop data for this page
|
||||
const savedCrop = cropperState.pageCrops[num];
|
||||
if (savedCrop) {
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropData = {
|
||||
x: savedCrop.x * imageData.naturalWidth,
|
||||
y: savedCrop.y * imageData.naturalHeight,
|
||||
width: savedCrop.width * imageData.naturalWidth,
|
||||
height: savedCrop.height * imageData.naturalHeight,
|
||||
};
|
||||
cropperState.cropper.setData(cropData);
|
||||
}
|
||||
|
||||
updatePageInfo();
|
||||
enableControls();
|
||||
hideLoader();
|
||||
showAlert('Ready', 'Please select an area to crop.');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error rendering page:", error);
|
||||
showAlert('Error', 'Failed to render page.');
|
||||
hideLoader();
|
||||
if (cropperState.cropper) {
|
||||
cropperState.cropper.destroy();
|
||||
}
|
||||
|
||||
const image = document.createElement('img');
|
||||
image.src = tempCanvas.toDataURL('image/png');
|
||||
document.getElementById('cropper-container').innerHTML = '';
|
||||
document.getElementById('cropper-container').appendChild(image);
|
||||
|
||||
image.onload = () => {
|
||||
cropperState.cropper = new Cropper(image, {
|
||||
viewMode: 1,
|
||||
background: false,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
rotatable: false,
|
||||
zoomable: false,
|
||||
});
|
||||
|
||||
// Restore saved crop data for this page
|
||||
const savedCrop = cropperState.pageCrops[num];
|
||||
if (savedCrop) {
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropData = {
|
||||
x: savedCrop.x * imageData.naturalWidth,
|
||||
y: savedCrop.y * imageData.naturalHeight,
|
||||
width: savedCrop.width * imageData.naturalWidth,
|
||||
height: savedCrop.height * imageData.naturalHeight,
|
||||
};
|
||||
cropperState.cropper.setData(cropData);
|
||||
}
|
||||
|
||||
updatePageInfo();
|
||||
enableControls();
|
||||
hideLoader();
|
||||
showAlert('Ready', 'Please select an area to crop.');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error rendering page:', error);
|
||||
showAlert('Error', 'Failed to render page.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,211 +98,253 @@ async function displayPageAsImage(num: any) {
|
||||
* @param {number} offset -1 for previous, 1 for next.
|
||||
*/
|
||||
async function changePage(offset: any) {
|
||||
// Save the current page's crop before changing
|
||||
saveCurrentCrop();
|
||||
// Save the current page's crop before changing
|
||||
saveCurrentCrop();
|
||||
|
||||
const newPageNum = cropperState.currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
||||
cropperState.currentPageNum = newPageNum;
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
}
|
||||
const newPageNum = cropperState.currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
||||
cropperState.currentPageNum = newPageNum;
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePageInfo() {
|
||||
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
document.getElementById('page-info').textContent =
|
||||
`Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
}
|
||||
|
||||
function enableControls() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('crop-button').disabled = false;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled =
|
||||
cropperState.currentPageNum <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled =
|
||||
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('crop-button').disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a non-destructive crop by updating the page's crop box.
|
||||
*/
|
||||
async function performMetadataCrop(pdfToModify: any, cropData: any) {
|
||||
for (const pageNum in cropData) {
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
const page = pdfToModify.getPages()[pageNum - 1];
|
||||
const { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
const rotation = page.getRotation().angle;
|
||||
const crop = cropData[pageNum];
|
||||
for (const pageNum in cropData) {
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
const page = pdfToModify.getPages()[pageNum - 1];
|
||||
const { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
const rotation = page.getRotation().angle;
|
||||
const crop = cropData[pageNum];
|
||||
|
||||
const visualPdfWidth = pageWidth * crop.width;
|
||||
const visualPdfHeight = pageHeight * crop.height;
|
||||
const visualPdfX = pageWidth * crop.x;
|
||||
const visualPdfY = pageHeight * crop.y;
|
||||
const visualPdfWidth = pageWidth * crop.width;
|
||||
const visualPdfHeight = pageHeight * crop.height;
|
||||
const visualPdfX = pageWidth * crop.x;
|
||||
const visualPdfY = pageHeight * crop.y;
|
||||
|
||||
let finalX, finalY, finalWidth, finalHeight;
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
finalX = visualPdfY;
|
||||
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
case 180:
|
||||
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
case 270:
|
||||
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalY = visualPdfX;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
default:
|
||||
finalX = visualPdfX;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
}
|
||||
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
||||
let finalX, finalY, finalWidth, finalHeight;
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
finalX = visualPdfY;
|
||||
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
case 180:
|
||||
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
case 270:
|
||||
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalY = visualPdfX;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
default:
|
||||
finalX = visualPdfX;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
}
|
||||
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a destructive crop by flattening the selected area to an image.
|
||||
*/
|
||||
async function performFlatteningCrop(cropData: any) {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes
|
||||
);
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
const finalCtx = finalCanvas.getContext('2d');
|
||||
const crop = cropData[pageNum];
|
||||
const finalWidth = tempCanvas.width * crop.width;
|
||||
const finalHeight = tempCanvas.height * crop.height;
|
||||
finalCanvas.width = finalWidth;
|
||||
finalCanvas.height = finalHeight;
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
finalCtx.drawImage(
|
||||
tempCanvas,
|
||||
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
|
||||
finalWidth, finalHeight,
|
||||
0, 0, finalWidth, finalHeight
|
||||
);
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
const finalCtx = finalCanvas.getContext('2d');
|
||||
const crop = cropData[pageNum];
|
||||
const finalWidth = tempCanvas.width * crop.width;
|
||||
const finalHeight = tempCanvas.height * crop.height;
|
||||
finalCanvas.width = finalWidth;
|
||||
finalCanvas.height = finalHeight;
|
||||
|
||||
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
|
||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
||||
} else {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
finalCtx.drawImage(
|
||||
tempCanvas,
|
||||
tempCanvas.width * crop.x,
|
||||
tempCanvas.height * crop.y,
|
||||
finalWidth,
|
||||
finalHeight,
|
||||
0,
|
||||
0,
|
||||
finalWidth,
|
||||
finalHeight
|
||||
);
|
||||
|
||||
const pngBytes = await new Promise((res) =>
|
||||
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
|
||||
);
|
||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
newPage.drawImage(embeddedImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
});
|
||||
} else {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
|
||||
i,
|
||||
]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
return newPdfDoc;
|
||||
}
|
||||
return newPdfDoc;
|
||||
}
|
||||
|
||||
|
||||
export async function setupCropperTool() {
|
||||
if (state.files.length === 0) return;
|
||||
if (state.files.length === 0) return;
|
||||
|
||||
// Clear pageCrops on new file upload
|
||||
cropperState.pageCrops = {};
|
||||
// Clear pageCrops on new file upload
|
||||
cropperState.pageCrops = {};
|
||||
|
||||
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
||||
cropperState.originalPdfBytes = arrayBuffer;
|
||||
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
||||
cropperState.originalPdfBytes = arrayBuffer;
|
||||
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
try {
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
||||
try {
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
||||
|
||||
cropperState.pdfDoc = await loadingTask.promise;
|
||||
cropperState.currentPageNum = 1;
|
||||
cropperState.pdfDoc = await loadingTask.promise;
|
||||
cropperState.currentPageNum = 1;
|
||||
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
|
||||
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
|
||||
document.getElementById('next-page').addEventListener('click', () => changePage(1));
|
||||
document
|
||||
.getElementById('prev-page')
|
||||
.addEventListener('click', () => changePage(-1));
|
||||
document
|
||||
.getElementById('next-page')
|
||||
.addEventListener('click', () => changePage(1));
|
||||
|
||||
document.getElementById('crop-button').addEventListener('click', async () => {
|
||||
// Get the last known crop from the active page before processing
|
||||
saveCurrentCrop();
|
||||
document
|
||||
.getElementById('crop-button')
|
||||
.addEventListener('click', async () => {
|
||||
// Get the last known crop from the active page before processing
|
||||
saveCurrentCrop();
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isDestructive = document.getElementById('destructive-crop-toggle').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isApplyToAll = document.getElementById('apply-to-all-toggle').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isDestructive = document.getElementById(
|
||||
'destructive-crop-toggle'
|
||||
).checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isApplyToAll = document.getElementById(
|
||||
'apply-to-all-toggle'
|
||||
).checked;
|
||||
|
||||
let finalCropData = {};
|
||||
if (isApplyToAll) {
|
||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
||||
if (!currentCrop) {
|
||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||
return;
|
||||
}
|
||||
// Apply the active page's crop to all pages
|
||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||
finalCropData[i] = currentCrop;
|
||||
}
|
||||
} else {
|
||||
// If not applying to all, only process pages with saved crops
|
||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
||||
obj[key] = cropperState.pageCrops[key];
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
let finalCropData = {};
|
||||
if (isApplyToAll) {
|
||||
const currentCrop =
|
||||
cropperState.pageCrops[cropperState.currentPageNum];
|
||||
if (!currentCrop) {
|
||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||
return;
|
||||
}
|
||||
// Apply the active page's crop to all pages
|
||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||
finalCropData[i] = currentCrop;
|
||||
}
|
||||
} else {
|
||||
// If not applying to all, only process pages with saved crops
|
||||
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = cropperState.pageCrops[key];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(finalCropData).length === 0) {
|
||||
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
|
||||
return;
|
||||
}
|
||||
if (Object.keys(finalCropData).length === 0) {
|
||||
showAlert(
|
||||
'No Crop Area',
|
||||
'Please select an area on at least one page to crop.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Applying crop...');
|
||||
showLoader('Applying crop...');
|
||||
|
||||
try {
|
||||
let finalPdfBytes;
|
||||
if (isDestructive) {
|
||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||
finalPdfBytes = await newPdfDoc.save();
|
||||
} else {
|
||||
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
||||
await performMetadataCrop(pdfToModify, finalCropData);
|
||||
finalPdfBytes = await pdfToModify.save();
|
||||
}
|
||||
try {
|
||||
let finalPdfBytes;
|
||||
if (isDestructive) {
|
||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||
finalPdfBytes = await newPdfDoc.save();
|
||||
} else {
|
||||
const pdfToModify = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes
|
||||
);
|
||||
await performMetadataCrop(pdfToModify, finalCropData);
|
||||
finalPdfBytes = await pdfToModify.save();
|
||||
}
|
||||
|
||||
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
||||
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
|
||||
showAlert('Success', 'Crop complete! Your download has started.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred during cropping.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error setting up cropper tool:", error);
|
||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||
}
|
||||
const fileName = isDestructive
|
||||
? 'flattened_crop.pdf'
|
||||
: 'standard_crop.pdf';
|
||||
downloadFile(
|
||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||
fileName
|
||||
);
|
||||
showAlert('Success', 'Crop complete! Your download has started.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred during cropping.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up cropper tool:', error);
|
||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,63 +3,78 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function decrypt() {
|
||||
const file = state.files[0];
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const password = document.getElementById('password-input').value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
const file = state.files[0];
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const password = document.getElementById('password-input').value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData,
|
||||
password: password,
|
||||
}).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
|
||||
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during PDF decryption:", error);
|
||||
hideLoader();
|
||||
if (error.name === 'PasswordException') {
|
||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
||||
} else {
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
}
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Building unlocked PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
}
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during PDF decryption:', error);
|
||||
hideLoader();
|
||||
if (error.name === 'PasswordException') {
|
||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
||||
} else {
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,53 +5,66 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function deletePages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-delete').value;
|
||||
if (!pageInput) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
||||
return;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-delete').value;
|
||||
if (!pageInput) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
||||
return;
|
||||
}
|
||||
showLoader('Deleting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToDelete = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToDelete.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
showLoader('Deleting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToDelete = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToDelete.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToDelete.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
if (indicesToDelete.size >= totalPages) {
|
||||
showAlert('Invalid Input', 'You cannot delete all pages.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not delete pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (indicesToDelete.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (indicesToDelete.size >= totalPages) {
|
||||
showAlert('Invalid Input', 'You cannot delete all pages.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const indicesToKeep = Array.from(
|
||||
{ length: totalPages },
|
||||
(_, i) => i
|
||||
).filter((index) => !indicesToDelete.has(index));
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'deleted-pages.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not delete pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Sortable from 'sortablejs'
|
||||
import {icons, createIcons} from "lucide";
|
||||
import Sortable from 'sortablejs';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
const duplicateOrganizeState = {
|
||||
sortableInstances: {}
|
||||
sortableInstances: {},
|
||||
};
|
||||
|
||||
function initializePageGridSortable() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
||||
}
|
||||
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.duplicate-btn, .delete-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: function(evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function(evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.duplicate-btn, .delete-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,124 +41,139 @@ function initializePageGridSortable() {
|
||||
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
||||
*/
|
||||
function attachEventListeners(element: any) {
|
||||
// Re-number all visible page labels
|
||||
const renumberPages = () => {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const pages = grid.querySelectorAll('.page-number');
|
||||
pages.forEach((label, index) => {
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
label.textContent = index + 1;
|
||||
});
|
||||
};
|
||||
// Re-number all visible page labels
|
||||
const renumberPages = () => {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const pages = grid.querySelectorAll('.page-number');
|
||||
pages.forEach((label, index) => {
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
label.textContent = index + 1;
|
||||
});
|
||||
};
|
||||
|
||||
// Duplicate button listener
|
||||
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
const clone = element.cloneNode(true);
|
||||
element.after(clone);
|
||||
attachEventListeners(clone);
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
// Duplicate button listener
|
||||
element
|
||||
.querySelector('.duplicate-btn')
|
||||
.addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
const clone = element.cloneNode(true);
|
||||
element.after(clone);
|
||||
attachEventListeners(clone);
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
});
|
||||
|
||||
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
if (document.getElementById('page-grid').children.length > 1) {
|
||||
element.remove();
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
} else {
|
||||
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
|
||||
}
|
||||
});
|
||||
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
if (document.getElementById('page-grid').children.length > 1) {
|
||||
element.remove();
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
} else {
|
||||
showAlert(
|
||||
'Cannot Delete',
|
||||
'You cannot delete the last page of the document.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderDuplicateOrganizeThumbnails() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
showLoader('Rendering page previews...');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
showLoader('Rendering page previews...');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
grid.textContent = '';
|
||||
grid.textContent = '';
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
|
||||
.promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.originalPageIndex = i - 1;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.originalPageIndex = i - 1;
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className =
|
||||
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
imgContainer.appendChild(img);
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
imgContainer.appendChild(img);
|
||||
|
||||
const pageNumberSpan = document.createElement('span');
|
||||
pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||
pageNumberSpan.textContent = i.toString();
|
||||
const pageNumberSpan = document.createElement('span');
|
||||
pageNumberSpan.className =
|
||||
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||
pageNumberSpan.textContent = i.toString();
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
duplicateBtn.title = 'Duplicate Page';
|
||||
const duplicateIcon = document.createElement('i');
|
||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||
duplicateIcon.className = 'w-5 h-5';
|
||||
duplicateBtn.appendChild(duplicateIcon);
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className =
|
||||
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
duplicateBtn.title = 'Duplicate Page';
|
||||
const duplicateIcon = document.createElement('i');
|
||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||
duplicateIcon.className = 'w-5 h-5';
|
||||
duplicateBtn.appendChild(duplicateIcon);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
deleteBtn.title = 'Delete Page';
|
||||
const deleteIcon = document.createElement('i');
|
||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||
deleteIcon.className = 'w-5 h-5';
|
||||
deleteBtn.appendChild(deleteIcon);
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className =
|
||||
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
deleteBtn.title = 'Delete Page';
|
||||
const deleteIcon = document.createElement('i');
|
||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||
deleteIcon.className = 'w-5 h-5';
|
||||
deleteBtn.appendChild(deleteIcon);
|
||||
|
||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
||||
grid.appendChild(wrapper);
|
||||
attachEventListeners(wrapper);
|
||||
}
|
||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
||||
grid.appendChild(wrapper);
|
||||
attachEventListeners(wrapper);
|
||||
}
|
||||
|
||||
initializePageGridSortable();
|
||||
createIcons({icons});
|
||||
hideLoader();
|
||||
initializePageGridSortable();
|
||||
createIcons({ icons });
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
|
||||
export async function processAndSave() {
|
||||
showLoader('Building new PDF...');
|
||||
try {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||
showLoader('Building new PDF...');
|
||||
try {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const finalIndices = Array.from(finalPageElements).map(el => parseInt(el.dataset.originalPageIndex));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const finalIndices = Array.from(finalPageElements).map((el) =>
|
||||
parseInt(el.dataset.originalPageIndex)
|
||||
);
|
||||
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the new PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'organized.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the new PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,93 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName, PDFString } from "pdf-lib"
|
||||
|
||||
import { PDFName, PDFString } from 'pdf-lib';
|
||||
|
||||
export async function editMetadata() {
|
||||
showLoader('Updating metadata...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const keywords = document.getElementById('meta-keywords').value;
|
||||
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
|
||||
showLoader('Updating metadata...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const creationDate = document.getElementById('meta-creation-date').value;
|
||||
if (creationDate) {
|
||||
state.pdfDoc.setCreationDate(new Date(creationDate));
|
||||
}
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const keywords = document.getElementById('meta-keywords').value;
|
||||
state.pdfDoc.setKeywords(
|
||||
keywords
|
||||
.split(',')
|
||||
.map((k: any) => k.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const modDate = document.getElementById('meta-mod-date').value;
|
||||
if (modDate) {
|
||||
state.pdfDoc.setModificationDate(new Date(modDate));
|
||||
} else {
|
||||
state.pdfDoc.setModificationDate(new Date());
|
||||
}
|
||||
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
|
||||
|
||||
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
|
||||
|
||||
allKeys.forEach((key: any) => {
|
||||
if (!standardKeys.has(key)) {
|
||||
infoDict.delete(PDFName.of(key));
|
||||
}
|
||||
});
|
||||
|
||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||
|
||||
customKeys.forEach((keyInput, index) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const key = keyInput.value.trim();
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const value = customValues[index].value.trim();
|
||||
if (key && value) {
|
||||
// Now we add the fields to a clean slate
|
||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const creationDate = document.getElementById('meta-creation-date').value;
|
||||
if (creationDate) {
|
||||
state.pdfDoc.setCreationDate(new Date(creationDate));
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const modDate = document.getElementById('meta-mod-date').value;
|
||||
if (modDate) {
|
||||
state.pdfDoc.setModificationDate(new Date(modDate));
|
||||
} else {
|
||||
state.pdfDoc.setModificationDate(new Date());
|
||||
}
|
||||
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set([
|
||||
'Title',
|
||||
'Author',
|
||||
'Subject',
|
||||
'Keywords',
|
||||
'Creator',
|
||||
'Producer',
|
||||
'CreationDate',
|
||||
'ModDate',
|
||||
]);
|
||||
|
||||
const allKeys = infoDict
|
||||
.keys()
|
||||
.map((key: any) => key.asString().substring(1)); // Clean keys
|
||||
|
||||
allKeys.forEach((key: any) => {
|
||||
if (!standardKeys.has(key)) {
|
||||
infoDict.delete(PDFName.of(key));
|
||||
}
|
||||
});
|
||||
|
||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||
|
||||
customKeys.forEach((keyInput, index) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const key = keyInput.value.trim();
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const value = customValues[index].value.trim();
|
||||
if (key && value) {
|
||||
// Now we add the fields to a clean slate
|
||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'metadata-edited.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not update metadata. Please check that date formats are correct.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,71 +3,84 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function encrypt() {
|
||||
const file = state.files[0];
|
||||
const password = (document.getElementById('password-input') as HTMLInputElement).value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter a password.');
|
||||
return;
|
||||
const file = state.files[0];
|
||||
const password = (
|
||||
document.getElementById('password-input') as HTMLInputElement
|
||||
).value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter a password.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer })
|
||||
.promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||
userPassword: password,
|
||||
ownerPassword: password,
|
||||
permissions: {
|
||||
printing: 'highResolution',
|
||||
modifying: false,
|
||||
copying: false,
|
||||
annotating: false,
|
||||
fillingForms: false,
|
||||
contentAccessibility: true,
|
||||
documentAssembly: false
|
||||
}
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during PDF encryption:", error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Encrypting and building PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||
userPassword: password,
|
||||
ownerPassword: password,
|
||||
permissions: {
|
||||
printing: 'highResolution',
|
||||
modifying: false,
|
||||
copying: false,
|
||||
annotating: false,
|
||||
fillingForms: false,
|
||||
contentAccessibility: true,
|
||||
documentAssembly: false,
|
||||
},
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
}
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,56 +6,65 @@ import JSZip from 'jszip';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function extractPages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-extract').value;
|
||||
if (!pageInput.trim()) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
||||
return;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-extract').value;
|
||||
if (!pageInput.trim()) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
||||
return;
|
||||
}
|
||||
showLoader('Extracting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToExtract = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
showLoader('Extracting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToExtract = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToExtract.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
||||
|
||||
for (const index of sortedIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const newPdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-pages.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not extract pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (indicesToExtract.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
||||
|
||||
for (const index of sortedIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||
index as number,
|
||||
]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const newPdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-pages.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not extract pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,84 +5,108 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupFixDimensionsUI() {
|
||||
const targetSizeSelect = document.getElementById('target-size');
|
||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||
if (targetSizeSelect && customSizeWrapper) {
|
||||
targetSizeSelect.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
customSizeWrapper.classList.toggle('hidden', targetSizeSelect.value !== 'Custom');
|
||||
});
|
||||
}
|
||||
const targetSizeSelect = document.getElementById('target-size');
|
||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||
if (targetSizeSelect && customSizeWrapper) {
|
||||
targetSizeSelect.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
customSizeWrapper.classList.toggle(
|
||||
'hidden',
|
||||
targetSizeSelect.value !== 'Custom'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function fixDimensions() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const targetSizeKey = document.getElementById('target-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColor = hexToRgb(document.getElementById('background-color').value);
|
||||
|
||||
showLoader('Standardizing pages...');
|
||||
try {
|
||||
let targetWidth, targetHeight;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const targetSizeKey = document.getElementById('target-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const scalingMode = document.querySelector(
|
||||
'input[name="scaling-mode"]:checked'
|
||||
).value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColor = hexToRgb(
|
||||
document.getElementById('background-color').value
|
||||
);
|
||||
|
||||
if (targetSizeKey === 'Custom') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const width = parseFloat(document.getElementById('custom-width').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const height = parseFloat(document.getElementById('custom-height').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const units = document.getElementById('custom-units').value;
|
||||
if (units === 'in') {
|
||||
targetWidth = width * 72;
|
||||
targetHeight = height * 72;
|
||||
} else { // mm
|
||||
targetWidth = width * (72 / 25.4);
|
||||
targetHeight = height * (72 / 25.4);
|
||||
}
|
||||
} else {
|
||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
||||
}
|
||||
showLoader('Standardizing pages...');
|
||||
try {
|
||||
let targetWidth, targetHeight;
|
||||
|
||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const sourcePage of sourceDoc.getPages()) {
|
||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
|
||||
|
||||
const scaleX = targetWidth / sourceWidth;
|
||||
const scaleY = targetHeight / sourceHeight;
|
||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sourceWidth * scale;
|
||||
const scaledHeight = sourceHeight * scale;
|
||||
|
||||
const x = (targetWidth - scaledWidth) / 2;
|
||||
const y = (targetHeight - scaledHeight) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (targetSizeKey === 'Custom') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const width = parseFloat(document.getElementById('custom-width').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const height = parseFloat(document.getElementById('custom-height').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const units = document.getElementById('custom-units').value;
|
||||
if (units === 'in') {
|
||||
targetWidth = width * 72;
|
||||
targetHeight = height * 72;
|
||||
} else {
|
||||
// mm
|
||||
targetWidth = width * (72 / 25.4);
|
||||
targetHeight = height * (72 / 25.4);
|
||||
}
|
||||
} else {
|
||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const sourcePage of sourceDoc.getPages()) {
|
||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
|
||||
const scaleX = targetWidth / sourceWidth;
|
||||
const scaleY = targetHeight / sourceHeight;
|
||||
const scale =
|
||||
scalingMode === 'fit'
|
||||
? Math.min(scaleX, scaleY)
|
||||
: Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sourceWidth * scale;
|
||||
const scaledHeight = sourceHeight * scale;
|
||||
|
||||
const x = (targetWidth - scaledWidth) / 2;
|
||||
const y = (targetHeight - scaledHeight) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'standardized.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,31 @@ import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function flatten() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Flattening PDF...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
form.flatten();
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Flattening PDF...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
form.flatten();
|
||||
|
||||
const flattenedBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.message.includes('getForm')) {
|
||||
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
|
||||
} else {
|
||||
showAlert('Error', 'Could not flatten the PDF.');
|
||||
}
|
||||
} finally {
|
||||
hideLoader();
|
||||
const flattenedBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
||||
'flattened.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.message.includes('getForm')) {
|
||||
showAlert(
|
||||
'No Form Found',
|
||||
'This PDF does not contain any form fields to flatten.'
|
||||
);
|
||||
} else {
|
||||
showAlert('Error', 'Could not flatten the PDF.');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
PDFTextField,
|
||||
PDFCheckBox,
|
||||
PDFRadioGroup,
|
||||
PDFDropdown,
|
||||
PDFButton,
|
||||
PDFSignature,
|
||||
PDFOptionList
|
||||
PDFDocument as PDFLibDocument,
|
||||
PDFTextField,
|
||||
PDFCheckBox,
|
||||
PDFRadioGroup,
|
||||
PDFDropdown,
|
||||
PDFButton,
|
||||
PDFSignature,
|
||||
PDFOptionList,
|
||||
} from 'pdf-lib';
|
||||
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
let pdfJsDoc: any = null;
|
||||
@@ -24,351 +24,380 @@ let currentPageNum = 1;
|
||||
let pdfRendering = false;
|
||||
let renderTimeout: any = null;
|
||||
const formState = {
|
||||
scale: 2,
|
||||
fields: [],
|
||||
scale: 2,
|
||||
fields: [],
|
||||
};
|
||||
|
||||
let fieldValues: Record<string, any> = {};
|
||||
|
||||
async function renderPage() {
|
||||
if (pdfRendering || !pdfJsDoc) return;
|
||||
if (pdfRendering || !pdfJsDoc) return;
|
||||
|
||||
pdfRendering = true;
|
||||
showLoader(`Rendering page ${currentPageNum}...`);
|
||||
pdfRendering = true;
|
||||
showLoader(`Rendering page ${currentPageNum}...`);
|
||||
|
||||
const page = await pdfJsDoc.getPage(currentPageNum);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const page = await pdfJsDoc.getPage(currentPageNum);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
|
||||
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
console.error('Could not get canvas context');
|
||||
pdfRendering = false;
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
canvas.style.transformOrigin = 'top left';
|
||||
canvas.style.transform = `scale(${formState.scale})`;
|
||||
|
||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
|
||||
const form = tempPdfDoc.getForm();
|
||||
Object.keys(fieldValues).forEach(fieldName => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
if (!field) return;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (fieldValues[fieldName] === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
if (Array.isArray(fieldValues[fieldName])) {
|
||||
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error applying value to field "${fieldName}":`, e);
|
||||
}
|
||||
});
|
||||
|
||||
const tempPdfBytes = await tempPdfDoc.save();
|
||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||
|
||||
await tempPage.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
} as any).promise;
|
||||
|
||||
const currentPageDisplay = document.getElementById('current-page-display');
|
||||
const totalPagesDisplay = document.getElementById('total-pages-display');
|
||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||
|
||||
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
|
||||
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
console.error('Could not get canvas context');
|
||||
pdfRendering = false;
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
canvas.style.transformOrigin = 'top left';
|
||||
canvas.style.transform = `scale(${formState.scale})`;
|
||||
|
||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const form = tempPdfDoc.getForm();
|
||||
Object.keys(fieldValues).forEach((fieldName) => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
if (!field) return;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (fieldValues[fieldName] === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
if (Array.isArray(fieldValues[fieldName])) {
|
||||
(fieldValues[fieldName] as any[]).forEach((option) =>
|
||||
field.select(option)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error applying value to field "${fieldName}":`, e);
|
||||
}
|
||||
});
|
||||
|
||||
const tempPdfBytes = await tempPdfDoc.save();
|
||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes })
|
||||
.promise;
|
||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||
|
||||
await tempPage.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
} as any).promise;
|
||||
|
||||
const currentPageDisplay = document.getElementById('current-page-display');
|
||||
const totalPagesDisplay = document.getElementById('total-pages-display');
|
||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||
|
||||
if (currentPageDisplay)
|
||||
currentPageDisplay.textContent = String(currentPageNum);
|
||||
if (totalPagesDisplay)
|
||||
totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||
|
||||
pdfRendering = false;
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
async function changePage(offset: number) {
|
||||
const newPageNum = currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
||||
currentPageNum = newPageNum;
|
||||
await renderPage();
|
||||
}
|
||||
const newPageNum = currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
||||
currentPageNum = newPageNum;
|
||||
await renderPage();
|
||||
}
|
||||
}
|
||||
|
||||
async function setZoom(factor: number) {
|
||||
formState.scale = factor;
|
||||
await renderPage();
|
||||
formState.scale = factor;
|
||||
await renderPage();
|
||||
}
|
||||
|
||||
function handleFormChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement | HTMLSelectElement;
|
||||
const name = input.name;
|
||||
let value: any;
|
||||
const input = event.target as HTMLInputElement | HTMLSelectElement;
|
||||
const name = input.name;
|
||||
let value: any;
|
||||
|
||||
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
||||
value = input.checked ? 'on' : 'off';
|
||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||
value = Array.from(input.options)
|
||||
.filter(option => option.selected)
|
||||
.map(option => option.value);
|
||||
} else {
|
||||
value = input.value;
|
||||
}
|
||||
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
||||
value = input.checked ? 'on' : 'off';
|
||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||
value = Array.from(input.options)
|
||||
.filter((option) => option.selected)
|
||||
.map((option) => option.value);
|
||||
} else {
|
||||
value = input.value;
|
||||
}
|
||||
|
||||
fieldValues[name] = value;
|
||||
fieldValues[name] = value;
|
||||
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(() => {
|
||||
renderPage();
|
||||
}, 350);
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(() => {
|
||||
renderPage();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function createFormFieldHtml(field: any): HTMLElement {
|
||||
const name = field.getName();
|
||||
const isRequired = field.isRequired();
|
||||
const labelText = name.replace(/[_-]/g, ' ');
|
||||
const name = field.getName();
|
||||
const isRequired = field.isRequired();
|
||||
const labelText = name.replace(/[_-]/g, ' ');
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `field-${name}`;
|
||||
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
|
||||
label.textContent = labelText;
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `field-${name}`;
|
||||
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
|
||||
label.textContent = labelText;
|
||||
|
||||
if (isRequired) {
|
||||
const requiredSpan = document.createElement('span');
|
||||
requiredSpan.className = 'text-red-500';
|
||||
requiredSpan.textContent = ' *';
|
||||
label.appendChild(requiredSpan);
|
||||
if (isRequired) {
|
||||
const requiredSpan = document.createElement('span');
|
||||
requiredSpan.className = 'text-red-500';
|
||||
requiredSpan.textContent = ' *';
|
||||
label.appendChild(requiredSpan);
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
|
||||
let inputElement: HTMLElement | DocumentFragment;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
fieldValues[name] = field.getText() || '';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `field-${name}`;
|
||||
input.name = name;
|
||||
input.value = fieldValues[name];
|
||||
input.className =
|
||||
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||
inputElement = input;
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.id = `field-${name}`;
|
||||
input.name = name;
|
||||
input.className =
|
||||
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||
input.checked = field.isChecked();
|
||||
inputElement = input;
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
fieldValues[name] = field.getSelected();
|
||||
const options = field.getOptions();
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach((opt: string) => {
|
||||
const optionLabel = document.createElement('label');
|
||||
optionLabel.className = 'flex items-center gap-2';
|
||||
|
||||
const radio = document.createElement('input');
|
||||
radio.type = 'radio';
|
||||
radio.name = name;
|
||||
radio.value = opt;
|
||||
radio.className =
|
||||
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||
if (opt === field.getSelected()) radio.checked = true;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'text-gray-300 text-sm';
|
||||
span.textContent = opt;
|
||||
|
||||
optionLabel.append(radio, span);
|
||||
fragment.appendChild(optionLabel);
|
||||
});
|
||||
inputElement = fragment;
|
||||
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
|
||||
const selectedValues = field.getSelected();
|
||||
fieldValues[name] = selectedValues;
|
||||
const options = field.getOptions();
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.id = `field-${name}`;
|
||||
select.name = name;
|
||||
select.className =
|
||||
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||
|
||||
if (field instanceof PDFOptionList) {
|
||||
select.multiple = true;
|
||||
select.size = Math.min(10, options.length);
|
||||
select.classList.add('h-auto');
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
|
||||
let inputElement: HTMLElement | DocumentFragment;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
fieldValues[name] = field.getText() || '';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `field-${name}`;
|
||||
input.name = name;
|
||||
input.value = fieldValues[name];
|
||||
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||
inputElement = input;
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.id = `field-${name}`;
|
||||
input.name = name;
|
||||
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||
input.checked = field.isChecked();
|
||||
inputElement = input;
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
fieldValues[name] = field.getSelected();
|
||||
const options = field.getOptions();
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach((opt: string) => {
|
||||
const optionLabel = document.createElement('label');
|
||||
optionLabel.className = 'flex items-center gap-2';
|
||||
|
||||
const radio = document.createElement('input');
|
||||
radio.type = 'radio';
|
||||
radio.name = name;
|
||||
radio.value = opt;
|
||||
radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||
if (opt === field.getSelected()) radio.checked = true;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'text-gray-300 text-sm';
|
||||
span.textContent = opt;
|
||||
|
||||
optionLabel.append(radio, span);
|
||||
fragment.appendChild(optionLabel);
|
||||
});
|
||||
inputElement = fragment;
|
||||
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
|
||||
const selectedValues = field.getSelected();
|
||||
fieldValues[name] = selectedValues;
|
||||
const options = field.getOptions();
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.id = `field-${name}`;
|
||||
select.name = name;
|
||||
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||
|
||||
if (field instanceof PDFOptionList) {
|
||||
select.multiple = true;
|
||||
select.size = Math.min(10, options.length);
|
||||
select.classList.add('h-auto');
|
||||
}
|
||||
|
||||
options.forEach((opt: string) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = opt;
|
||||
option.textContent = opt;
|
||||
if (selectedValues.includes(opt)) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
inputElement = select;
|
||||
options.forEach((opt: string) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = opt;
|
||||
option.textContent = opt;
|
||||
if (selectedValues.includes(opt)) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
inputElement = select;
|
||||
} else {
|
||||
const unsupportedDiv = document.createElement('div');
|
||||
unsupportedDiv.className =
|
||||
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-400';
|
||||
if (field instanceof PDFSignature) {
|
||||
p.textContent = 'Signature field: Not supported for direct editing.';
|
||||
} else if (field instanceof PDFButton) {
|
||||
p.textContent = `Button: ${labelText}`;
|
||||
} else {
|
||||
const unsupportedDiv = document.createElement('div');
|
||||
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-400';
|
||||
if (field instanceof PDFSignature) {
|
||||
p.textContent = 'Signature field: Not supported for direct editing.';
|
||||
} else if (field instanceof PDFButton) {
|
||||
p.textContent = `Button: ${labelText}`;
|
||||
} else {
|
||||
p.textContent = `Unsupported field type: ${field.constructor.name}`;
|
||||
}
|
||||
unsupportedDiv.appendChild(p);
|
||||
return unsupportedDiv;
|
||||
p.textContent = `Unsupported field type: ${field.constructor.name}`;
|
||||
}
|
||||
unsupportedDiv.appendChild(p);
|
||||
return unsupportedDiv;
|
||||
}
|
||||
|
||||
wrapper.appendChild(inputElement);
|
||||
return wrapper;
|
||||
wrapper.appendChild(inputElement);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export async function setupFormFiller() {
|
||||
if (!state.pdfDoc) return;
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
showLoader('Analyzing form fields...');
|
||||
const formContainer = document.getElementById('form-fields-container');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
showLoader('Analyzing form fields...');
|
||||
const formContainer = document.getElementById('form-fields-container');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!formContainer || !processBtn) {
|
||||
console.error('Required DOM elements not found');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
if (!formContainer || !processBtn) {
|
||||
console.error('Required DOM elements not found');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
const fields = form.getFields();
|
||||
formState.fields = fields;
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
const fields = form.getFields();
|
||||
formState.fields = fields;
|
||||
|
||||
formContainer.textContent = '';
|
||||
formContainer.textContent = '';
|
||||
|
||||
if (fields.length === 0) {
|
||||
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||
processBtn.classList.add('hidden');
|
||||
} else {
|
||||
fields.forEach((field: any) => {
|
||||
try {
|
||||
const fieldElement = createFormFieldHtml(field);
|
||||
formContainer.appendChild(fieldElement);
|
||||
} catch (e: any) {
|
||||
console.error(`Error processing field "${field.getName()}":`, e);
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
// Sanitize error message display
|
||||
const p1 = document.createElement('p');
|
||||
p1.className = 'text-sm text-gray-500';
|
||||
p1.textContent = `Unsupported field: ${field.getName()}`;
|
||||
const p2 = document.createElement('p');
|
||||
p2.className = 'text-xs text-gray-500';
|
||||
p2.textContent = e.message;
|
||||
errorDiv.append(p1, p2);
|
||||
formContainer.appendChild(errorDiv);
|
||||
}
|
||||
});
|
||||
|
||||
processBtn.classList.remove('hidden');
|
||||
formContainer.addEventListener('change', handleFormChange);
|
||||
formContainer.addEventListener('input', handleFormChange);
|
||||
if (fields.length === 0) {
|
||||
formContainer.innerHTML =
|
||||
'<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||
processBtn.classList.add('hidden');
|
||||
} else {
|
||||
fields.forEach((field: any) => {
|
||||
try {
|
||||
const fieldElement = createFormFieldHtml(field);
|
||||
formContainer.appendChild(fieldElement);
|
||||
} catch (e: any) {
|
||||
console.error(`Error processing field "${field.getName()}":`, e);
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className =
|
||||
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||
// Sanitize error message display
|
||||
const p1 = document.createElement('p');
|
||||
p1.className = 'text-sm text-gray-500';
|
||||
p1.textContent = `Unsupported field: ${field.getName()}`;
|
||||
const p2 = document.createElement('p');
|
||||
p2.className = 'text-xs text-gray-500';
|
||||
p2.textContent = e.message;
|
||||
errorDiv.append(p1, p2);
|
||||
formContainer.appendChild(errorDiv);
|
||||
}
|
||||
});
|
||||
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
currentPageNum = 1;
|
||||
await renderPage();
|
||||
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
const prevPageBtn = document.getElementById('prev-page');
|
||||
const nextPageBtn = document.getElementById('next-page');
|
||||
|
||||
if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25));
|
||||
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25)));
|
||||
if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1));
|
||||
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
||||
|
||||
hideLoader();
|
||||
|
||||
const formFillerOptions = document.getElementById('form-filler-options');
|
||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||
|
||||
} catch (e) {
|
||||
console.error("Critical error setting up form filler:", e);
|
||||
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
|
||||
hideLoader();
|
||||
processBtn.classList.remove('hidden');
|
||||
formContainer.addEventListener('change', handleFormChange);
|
||||
formContainer.addEventListener('input', handleFormChange);
|
||||
}
|
||||
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
currentPageNum = 1;
|
||||
await renderPage();
|
||||
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
const prevPageBtn = document.getElementById('prev-page');
|
||||
const nextPageBtn = document.getElementById('next-page');
|
||||
|
||||
if (zoomInBtn)
|
||||
zoomInBtn.addEventListener('click', () =>
|
||||
setZoom(formState.scale + 0.25)
|
||||
);
|
||||
if (zoomOutBtn)
|
||||
zoomOutBtn.addEventListener('click', () =>
|
||||
setZoom(Math.max(1, formState.scale - 0.25))
|
||||
);
|
||||
if (prevPageBtn)
|
||||
prevPageBtn.addEventListener('click', () => changePage(-1));
|
||||
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
||||
|
||||
hideLoader();
|
||||
|
||||
const formFillerOptions = document.getElementById('form-filler-options');
|
||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error('Critical error setting up form filler:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to read PDF form data. The file may be corrupt or not a valid form.'
|
||||
);
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function processAndDownloadForm() {
|
||||
showLoader('Applying form data...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
showLoader('Applying form data...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
|
||||
Object.keys(fieldValues).forEach(fieldName => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
const value = fieldValues[fieldName];
|
||||
Object.keys(fieldValues).forEach((fieldName) => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
const value = fieldValues[fieldName];
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(value);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (value === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(option => field.select(option));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error processing field "${fieldName}" during download:`, e);
|
||||
}
|
||||
});
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(value);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (value === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((option) => field.select(option));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Error processing field "${fieldName}" during download:`,
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'filled-form.pdf'
|
||||
);
|
||||
|
||||
showAlert('Success', 'Form has been filled and downloaded.');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the filled form.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
showAlert('Success', 'Form has been filled and downloaded.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the filled form.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,31 +5,44 @@ import heic2any from 'heic2any';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function heicToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: 'image/png',
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult)
|
||||
? conversionResult[0]
|
||||
: conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: "image/png",
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_heic.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,12 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
|
||||
if (!jpegBlob)
|
||||
return reject(new Error('Canvas to JPEG conversion failed.'));
|
||||
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
||||
}, 'image/jpeg', 0.9
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
@@ -56,12 +59,11 @@ function sanitizeImageAsPng(imageBytes: any) {
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob(
|
||||
async (pngBlob) => {
|
||||
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
|
||||
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
||||
}, 'image/png'
|
||||
);
|
||||
canvas.toBlob(async (pngBlob) => {
|
||||
if (!pngBlob)
|
||||
return reject(new Error('Canvas to PNG conversion failed.'));
|
||||
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
||||
}, 'image/png');
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
img.onerror = () => {
|
||||
@@ -72,62 +74,77 @@ function sanitizeImageAsPng(imageBytes: any) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function imageToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one image file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting images to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const imageList = document.getElementById('image-list');
|
||||
const sortedFiles = Array.from(imageList.children)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map(li => state.files.find(f => f.name === li.dataset.fileName))
|
||||
.filter(Boolean);
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one image file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting images to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const imageList = document.getElementById('image-list');
|
||||
const sortedFiles = Array.from(imageList.children)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
|
||||
.filter(Boolean);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
let image;
|
||||
for (const file of sortedFiles) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
let image;
|
||||
|
||||
if (file.type === 'image/jpeg') {
|
||||
try {
|
||||
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
} else if (file.type === 'image/png') {
|
||||
try {
|
||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
} else {
|
||||
// For WebP and other types, convert to PNG to preserve transparency
|
||||
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([image.width, image.height]);
|
||||
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
if (file.type === 'image/jpeg') {
|
||||
try {
|
||||
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`
|
||||
);
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
if (pdfDoc.getPageCount() === 0) {
|
||||
throw new Error("No valid images could be processed. Please check your files.");
|
||||
} else if (file.type === 'image/png') {
|
||||
try {
|
||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
|
||||
);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
} else {
|
||||
// For WebP and other types, convert to PNG to preserve transparency
|
||||
console.warn(
|
||||
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
|
||||
);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from-images.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const page = pdfDoc.addPage([image.width, image.height]);
|
||||
page.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
|
||||
if (pdfDoc.getPageCount() === 0) {
|
||||
throw new Error(
|
||||
'No valid images could be processed. Please check your files.'
|
||||
);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from-images.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,68 +49,83 @@ import { setupCompareTool } from './compare-pdfs.js';
|
||||
import { setupOcrTool } from './ocr-pdf.js';
|
||||
import { wordToPdf } from './word-to-pdf.js';
|
||||
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
||||
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
|
||||
import {
|
||||
removeAnnotations,
|
||||
setupRemoveAnnotationsTool,
|
||||
} from './remove-annotations.js';
|
||||
import { setupCropperTool } from './cropper.js';
|
||||
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
||||
import { posterize, setupPosterizeTool } from './posterize.js';
|
||||
import { removeBlankPages, setupRemoveBlankPagesTool } from './remove-blank-pages.js';
|
||||
import {
|
||||
removeBlankPages,
|
||||
setupRemoveBlankPagesTool,
|
||||
} from './remove-blank-pages.js';
|
||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
split: { process: split, setup: setupSplitTool },
|
||||
encrypt,
|
||||
decrypt,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
'pdf-to-jpg': pdfToJpg,
|
||||
'jpg-to-pdf': jpgToPdf,
|
||||
'scan-to-pdf': scanToPdf,
|
||||
compress,
|
||||
'pdf-to-greyscale': pdfToGreyscale,
|
||||
'pdf-to-zip': pdfToZip,
|
||||
'edit-metadata': editMetadata,
|
||||
'remove-metadata': removeMetadata,
|
||||
flatten,
|
||||
'pdf-to-png': pdfToPng,
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': deletePages,
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
'add-header-footer': addHeaderFooter,
|
||||
'image-to-pdf': imageToPdf,
|
||||
'change-permissions': changePermissions,
|
||||
'pdf-to-markdown': pdfToMarkdown,
|
||||
'txt-to-pdf': txtToPdf,
|
||||
'invert-colors': invertColors,
|
||||
'reverse-pages': reversePages,
|
||||
'md-to-pdf': mdToPdf,
|
||||
'svg-to-pdf': svgToPdf,
|
||||
'bmp-to-pdf': bmpToPdf,
|
||||
'heic-to-pdf': heicToPdf,
|
||||
'tiff-to-pdf': tiffToPdf,
|
||||
'pdf-to-bmp': pdfToBmp,
|
||||
'pdf-to-tiff': pdfToTiff,
|
||||
'split-in-half': splitInHalf,
|
||||
'page-dimensions': analyzeAndDisplayDimensions,
|
||||
'n-up': { process: nUpTool, setup: setupNUpUI },
|
||||
'duplicate-organize': { process: processAndSave },
|
||||
'combine-single-page': combineToSinglePage,
|
||||
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
||||
'change-background-color': changeBackgroundColor,
|
||||
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
||||
'compare-pdfs': { setup: setupCompareTool },
|
||||
'ocr-pdf': { setup: setupOcrTool },
|
||||
'word-to-pdf': wordToPdf,
|
||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
|
||||
'cropper': { setup: setupCropperTool },
|
||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller},
|
||||
'posterize': { process: posterize, setup: setupPosterizeTool },
|
||||
'remove-blank-pages': { process: removeBlankPages, setup: setupRemoveBlankPagesTool },
|
||||
'alternate-merge': { process: alternateMerge, setup: setupAlternateMergeTool },
|
||||
};
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
split: { process: split, setup: setupSplitTool },
|
||||
encrypt,
|
||||
decrypt,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
'pdf-to-jpg': pdfToJpg,
|
||||
'jpg-to-pdf': jpgToPdf,
|
||||
'scan-to-pdf': scanToPdf,
|
||||
compress,
|
||||
'pdf-to-greyscale': pdfToGreyscale,
|
||||
'pdf-to-zip': pdfToZip,
|
||||
'edit-metadata': editMetadata,
|
||||
'remove-metadata': removeMetadata,
|
||||
flatten,
|
||||
'pdf-to-png': pdfToPng,
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': deletePages,
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
'add-header-footer': addHeaderFooter,
|
||||
'image-to-pdf': imageToPdf,
|
||||
'change-permissions': changePermissions,
|
||||
'pdf-to-markdown': pdfToMarkdown,
|
||||
'txt-to-pdf': txtToPdf,
|
||||
'invert-colors': invertColors,
|
||||
'reverse-pages': reversePages,
|
||||
'md-to-pdf': mdToPdf,
|
||||
'svg-to-pdf': svgToPdf,
|
||||
'bmp-to-pdf': bmpToPdf,
|
||||
'heic-to-pdf': heicToPdf,
|
||||
'tiff-to-pdf': tiffToPdf,
|
||||
'pdf-to-bmp': pdfToBmp,
|
||||
'pdf-to-tiff': pdfToTiff,
|
||||
'split-in-half': splitInHalf,
|
||||
'page-dimensions': analyzeAndDisplayDimensions,
|
||||
'n-up': { process: nUpTool, setup: setupNUpUI },
|
||||
'duplicate-organize': { process: processAndSave },
|
||||
'combine-single-page': combineToSinglePage,
|
||||
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
||||
'change-background-color': changeBackgroundColor,
|
||||
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
||||
'compare-pdfs': { setup: setupCompareTool },
|
||||
'ocr-pdf': { setup: setupOcrTool },
|
||||
'word-to-pdf': wordToPdf,
|
||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||
'remove-annotations': {
|
||||
process: removeAnnotations,
|
||||
setup: setupRemoveAnnotationsTool,
|
||||
},
|
||||
cropper: { setup: setupCropperTool },
|
||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
||||
posterize: { process: posterize, setup: setupPosterizeTool },
|
||||
'remove-blank-pages': {
|
||||
process: removeBlankPages,
|
||||
setup: setupRemoveBlankPagesTool,
|
||||
},
|
||||
'alternate-merge': {
|
||||
process: alternateMerge,
|
||||
setup: setupAlternateMergeTool,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,49 +4,62 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function invertColors() {
|
||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
||||
showLoader('Inverting PDF colors...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Inverting PDF colors...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
data[j] = 255 - data[j]; // red
|
||||
data[j + 1] = 255 - data[j + 1]; // green
|
||||
data[j + 2] = 255 - data[j + 2]; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
data[j] = 255 - data[j]; // red
|
||||
data[j + 1] = 255 - data[j + 1]; // green
|
||||
data[j + 2] = 255 - data[j + 2]; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not invert PDF colors.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'inverted.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not invert PDF colors.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Takes any image byte array and uses the browser's canvas to convert it
|
||||
* Takes any image byte array and uses the browser's canvas to convert it
|
||||
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
|
||||
* @param {Uint8Array} imageBytes The raw bytes of the image file.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
|
||||
@@ -39,7 +39,11 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
@@ -63,13 +67,20 @@ export async function jpgToPdf() {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
|
||||
showAlert(
|
||||
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
|
||||
);
|
||||
try {
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
} catch (fallbackError) {
|
||||
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
|
||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
||||
console.error(
|
||||
`Failed to process ${file.name} after sanitization:`,
|
||||
fallbackError
|
||||
);
|
||||
throw new Error(
|
||||
`Could not process "${file.name}". The file may be corrupted.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +94,10 @@ export async function jpgToPdf() {
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_jpgs.pdf');
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_jpgs.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
|
||||
@@ -3,35 +3,41 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
|
||||
export async function mdToPdf() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
|
||||
showAlert('Libraries Not Ready', 'PDF generation libraries are loading. Please try again.');
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
if (
|
||||
typeof window.jspdf === 'undefined' ||
|
||||
typeof window.html2canvas === 'undefined'
|
||||
) {
|
||||
showAlert(
|
||||
'Libraries Not Ready',
|
||||
'PDF generation libraries are loading. Please try again.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const markdownContent = document.getElementById('md-input').value.trim();
|
||||
if (!markdownContent) {
|
||||
showAlert('Input Required', 'Please enter some Markdown text.');
|
||||
return;
|
||||
}
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
||||
const htmlContent = marked.parse(markdownContent);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const markdownContent = document.getElementById('md-input').value.trim();
|
||||
if (!markdownContent) {
|
||||
showAlert('Input Required', 'Please enter some Markdown text.');
|
||||
return;
|
||||
}
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
const pageFormat = document.getElementById('page-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const marginSize = document.getElementById('margin-size').value;
|
||||
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
||||
const htmlContent = marked.parse(markdownContent);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageFormat = document.getElementById('page-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const marginSize = document.getElementById('margin-size').value;
|
||||
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = `
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.cssText =
|
||||
'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = `
|
||||
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
||||
h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||
h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
||||
@@ -42,44 +48,48 @@ export async function mdToPdf() {
|
||||
table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
||||
img { max-width: 100%; }
|
||||
`;
|
||||
tempContainer.appendChild(styleSheet);
|
||||
tempContainer.innerHTML += htmlContent;
|
||||
document.body.appendChild(tempContainer);
|
||||
tempContainer.appendChild(styleSheet);
|
||||
tempContainer.innerHTML += htmlContent;
|
||||
document.body.appendChild(tempContainer);
|
||||
|
||||
const canvas = await html2canvas(tempContainer, { scale: 2, useCORS: true });
|
||||
document.body.removeChild(tempContainer);
|
||||
const canvas = await html2canvas(tempContainer, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
});
|
||||
document.body.removeChild(tempContainer);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
||||
const pageFormats = { 'a4': [210, 297], 'letter': [216, 279] };
|
||||
const format = pageFormats[pageFormat];
|
||||
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
|
||||
const margins = { 'narrow': 10, 'normal': 20, 'wide': 30 };
|
||||
const margin = margins[marginSize];
|
||||
const contentWidth = pageWidth - (margin * 2);
|
||||
const contentHeight = pageHeight - (margin * 2);
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
||||
const pageFormats = { a4: [210, 297], letter: [216, 279] };
|
||||
const format = pageFormats[pageFormat];
|
||||
const [pageWidth, pageHeight] =
|
||||
orientation === 'landscape' ? [format[1], format[0]] : format;
|
||||
const margins = { narrow: 10, normal: 20, wide: 30 };
|
||||
const margin = margins[marginSize];
|
||||
const contentWidth = pageWidth - margin * 2;
|
||||
const contentHeight = pageHeight - margin * 2;
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
||||
|
||||
let heightLeft = imgHeight;
|
||||
let position = margin;
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
let heightLeft = imgHeight;
|
||||
let position = margin;
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position = position - pageHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
}
|
||||
|
||||
const pdfBlob = pdf.output('blob');
|
||||
downloadFile(pdfBlob, 'markdown-document.pdf');
|
||||
} catch (error) {
|
||||
console.error('MD to PDF conversion error:', error);
|
||||
showAlert('Conversion Error', 'Failed to generate PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
while (heightLeft > 0) {
|
||||
position = position - pageHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBlob = pdf.output('blob');
|
||||
downloadFile(pdfBlob, 'markdown-document.pdf');
|
||||
} catch (error) {
|
||||
console.error('MD to PDF conversion error:', error);
|
||||
showAlert('Conversion Error', 'Failed to generate PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,390 +7,419 @@ import * as pdfjsLib from 'pdfjs-dist';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const mergeState = {
|
||||
pdfDocs: {},
|
||||
activeMode: 'file',
|
||||
sortableInstances: {},
|
||||
isRendering: false,
|
||||
cachedThumbnails: null,
|
||||
lastFileHash: null
|
||||
pdfDocs: {},
|
||||
activeMode: 'file',
|
||||
sortableInstances: {},
|
||||
isRendering: false,
|
||||
cachedThumbnails: null,
|
||||
lastFileHash: null,
|
||||
};
|
||||
|
||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
const indices = new Set();
|
||||
if (!rangeString.trim()) return [];
|
||||
const indices = new Set();
|
||||
if (!rangeString.trim()) return [];
|
||||
|
||||
const ranges = rangeString.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
const ranges = rangeString.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function initializeFileListSortable() {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) return;
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
if (mergeState.sortableInstances.fileList) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
if (mergeState.sortableInstances.fileList) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList.destroy();
|
||||
}
|
||||
mergeState.sortableInstances.fileList.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initializePageThumbnailsSortable() {
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
if (mergeState.sortableInstances.pageThumbnails) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
if (mergeState.sortableInstances.pageThumbnails) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails.destroy();
|
||||
}
|
||||
mergeState.sortableInstances.pageThumbnails.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function generateFileHash() {
|
||||
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
|
||||
return (state.files as File[])
|
||||
.map((f) => `${f.name}-${f.size}-${f.lastModified}`)
|
||||
.join('|');
|
||||
}
|
||||
|
||||
async function renderPageMergeThumbnails() {
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
|
||||
const currentFileHash = generateFileHash();
|
||||
const filesChanged = currentFileHash !== mergeState.lastFileHash;
|
||||
const currentFileHash = generateFileHash();
|
||||
const filesChanged = currentFileHash !== mergeState.lastFileHash;
|
||||
|
||||
if (!filesChanged && mergeState.cachedThumbnails !== null) {
|
||||
// Simple check to see if it's already rendered to avoid flicker.
|
||||
if (container.firstChild) {
|
||||
initializePageThumbnailsSortable();
|
||||
return;
|
||||
}
|
||||
if (!filesChanged && mergeState.cachedThumbnails !== null) {
|
||||
// Simple check to see if it's already rendered to avoid flicker.
|
||||
if (container.firstChild) {
|
||||
initializePageThumbnailsSortable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mergeState.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeState.isRendering = true;
|
||||
container.textContent = '';
|
||||
|
||||
let currentPageNumber = 0;
|
||||
let totalPages = state.files.reduce((sum, file) => {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||
}, 0);
|
||||
|
||||
try {
|
||||
const thumbnailsHTML = [];
|
||||
|
||||
for (const file of state.files) {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
if (!pdfDoc) continue;
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
currentPageNumber++;
|
||||
showLoader(
|
||||
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
||||
);
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d')!;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
canvas: canvas,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||
wrapper.dataset.fileName = file.name;
|
||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'relative';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||
|
||||
const pageNumDiv = document.createElement('div');
|
||||
pageNumDiv.className =
|
||||
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
||||
pageNumDiv.textContent = i.toString();
|
||||
|
||||
imgContainer.append(img, pageNumDiv);
|
||||
|
||||
const fileNamePara = document.createElement('p');
|
||||
fileNamePara.className =
|
||||
'text-xs text-gray-400 truncate w-full text-center';
|
||||
const fullTitle = `${file.name} (page ${i})`;
|
||||
fileNamePara.title = fullTitle;
|
||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
||||
|
||||
wrapper.append(imgContainer, fileNamePara);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
pdfjsDoc.destroy();
|
||||
}
|
||||
|
||||
if (mergeState.isRendering) {
|
||||
return;
|
||||
}
|
||||
mergeState.cachedThumbnails = true;
|
||||
mergeState.lastFileHash = currentFileHash;
|
||||
|
||||
mergeState.isRendering = true;
|
||||
container.textContent = '';
|
||||
|
||||
let currentPageNumber = 0;
|
||||
let totalPages = state.files.reduce((sum, file) => {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||
}, 0);
|
||||
|
||||
try {
|
||||
const thumbnailsHTML = [];
|
||||
|
||||
for (const file of state.files) {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
if (!pdfDoc) continue;
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
currentPageNumber++;
|
||||
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d')!;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
canvas: canvas,
|
||||
viewport
|
||||
}).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||
wrapper.dataset.fileName = file.name;
|
||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'relative';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||
|
||||
const pageNumDiv = document.createElement('div');
|
||||
pageNumDiv.className = 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
||||
pageNumDiv.textContent = i.toString();
|
||||
|
||||
imgContainer.append(img, pageNumDiv);
|
||||
|
||||
const fileNamePara = document.createElement('p');
|
||||
fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center';
|
||||
const fullTitle = `${file.name} (page ${i})`;
|
||||
fileNamePara.title = fullTitle;
|
||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
||||
|
||||
wrapper.append(imgContainer, fileNamePara);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
pdfjsDoc.destroy();
|
||||
}
|
||||
|
||||
mergeState.cachedThumbnails = true;
|
||||
mergeState.lastFileHash = currentFileHash;
|
||||
|
||||
initializePageThumbnailsSortable();
|
||||
} catch (error) {
|
||||
console.error('Error rendering page thumbnails:', error);
|
||||
showAlert('Error', 'Failed to render page thumbnails');
|
||||
} finally {
|
||||
hideLoader();
|
||||
mergeState.isRendering = false;
|
||||
}
|
||||
initializePageThumbnailsSortable();
|
||||
} catch (error) {
|
||||
console.error('Error rendering page thumbnails:', error);
|
||||
showAlert('Error', 'Failed to render page thumbnails');
|
||||
} finally {
|
||||
hideLoader();
|
||||
mergeState.isRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function merge() {
|
||||
showLoader('Merging PDFs...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
showLoader('Merging PDFs...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
if (mergeState.activeMode === 'file') {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const sortedFiles = Array.from(fileList.children).map(li => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
return state.files.find(f => f.name === li.dataset.fileName);
|
||||
}).filter(Boolean);
|
||||
if (mergeState.activeMode === 'file') {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const sortedFiles = Array.from(fileList.children)
|
||||
.map((li) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
||||
if (!rangeInput) continue;
|
||||
for (const file of sortedFiles) {
|
||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
||||
if (!rangeInput) continue;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInputValue = rangeInput.value;
|
||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
||||
if (!sourcePdf) continue;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInputValue = rangeInput.value;
|
||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
||||
if (!sourcePdf) continue;
|
||||
|
||||
const totalPages = sourcePdf.getPageCount();
|
||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||
const totalPages = sourcePdf.getPageCount();
|
||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||
|
||||
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
}
|
||||
const indicesToCopy =
|
||||
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
}
|
||||
} else {
|
||||
const pageContainer = document.getElementById('page-merge-preview');
|
||||
const pageElements = Array.from(pageContainer.children);
|
||||
|
||||
} else {
|
||||
const pageContainer = document.getElementById('page-merge-preview');
|
||||
const pageElements = Array.from(pageContainer.children);
|
||||
for (const el of pageElements) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const fileName = el.dataset.fileName;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
||||
|
||||
for (const el of pageElements) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const fileName = el.dataset.fileName;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
||||
|
||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||
if (sourcePdf && !isNaN(pageIndex)) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||
if (sourcePdf && !isNaN(pageIndex)) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
||||
pageIndex,
|
||||
]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf');
|
||||
showAlert('Success', 'PDFs merged successfully!');
|
||||
|
||||
} catch (e) {
|
||||
console.error('Merge error:', e);
|
||||
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||
'merged.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDFs merged successfully!');
|
||||
} catch (e) {
|
||||
console.error('Merge error:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupMergeTool() {
|
||||
document.getElementById('merge-options').classList.remove('hidden');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('process-btn').disabled = false;
|
||||
document.getElementById('merge-options').classList.remove('hidden');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('process-btn').disabled = false;
|
||||
|
||||
const wasInPageMode = mergeState.activeMode === 'page';
|
||||
const wasInPageMode = mergeState.activeMode === 'page';
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
if (!mergeState.pdfDocs[file.name]) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
showAlert('Error', 'Failed to load one or more PDF files');
|
||||
return;
|
||||
} finally {
|
||||
hideLoader();
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
if (!mergeState.pdfDocs[file.name]) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
|
||||
pdfBytes as ArrayBuffer,
|
||||
{
|
||||
ignoreEncryption: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
showAlert('Error', 'Failed to load one or more PDF files');
|
||||
return;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
const fileModeBtn = document.getElementById('file-mode-btn');
|
||||
const pageModeBtn = document.getElementById('page-mode-btn');
|
||||
const filePanel = document.getElementById('file-mode-panel');
|
||||
const pagePanel = document.getElementById('page-mode-panel');
|
||||
const fileList = document.getElementById('file-list');
|
||||
const fileModeBtn = document.getElementById('file-mode-btn');
|
||||
const pageModeBtn = document.getElementById('page-mode-btn');
|
||||
const filePanel = document.getElementById('file-mode-panel');
|
||||
const pagePanel = document.getElementById('page-mode-panel');
|
||||
const fileList = document.getElementById('file-list');
|
||||
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach(f => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach((f) => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||
li.dataset.fileName = f.name;
|
||||
const li = document.createElement('li');
|
||||
li.className =
|
||||
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||
li.dataset.fileName = f.name;
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.className = 'flex items-center justify-between';
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.className = 'flex items-center justify-between';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
|
||||
nameSpan.title = f.name;
|
||||
nameSpan.textContent = f.name;
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
|
||||
nameSpan.title = f.name;
|
||||
nameSpan.textContent = f.name;
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className =
|
||||
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
||||
|
||||
mainDiv.append(nameSpan, dragHandle);
|
||||
mainDiv.append(nameSpan, dragHandle);
|
||||
|
||||
const rangeDiv = document.createElement('div');
|
||||
rangeDiv.className = 'mt-2';
|
||||
const rangeDiv = document.createElement('div');
|
||||
rangeDiv.className = 'mt-2';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `range-${safeFileName}`;
|
||||
label.className = 'text-xs text-gray-400';
|
||||
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `range-${safeFileName}`;
|
||||
label.className = 'text-xs text-gray-400';
|
||||
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `range-${safeFileName}`;
|
||||
input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||
input.placeholder = 'Leave blank for all pages';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `range-${safeFileName}`;
|
||||
input.className =
|
||||
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||
input.placeholder = 'Leave blank for all pages';
|
||||
|
||||
rangeDiv.append(label, input);
|
||||
li.append(mainDiv, rangeDiv);
|
||||
fileList.appendChild(li);
|
||||
});
|
||||
rangeDiv.append(label, input);
|
||||
li.append(mainDiv, rangeDiv);
|
||||
fileList.appendChild(li);
|
||||
});
|
||||
|
||||
initializeFileListSortable();
|
||||
initializeFileListSortable();
|
||||
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
||||
fileModeBtn.replaceWith(newFileModeBtn);
|
||||
pageModeBtn.replaceWith(newPageModeBtn);
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
||||
fileModeBtn.replaceWith(newFileModeBtn);
|
||||
pageModeBtn.replaceWith(newPageModeBtn);
|
||||
|
||||
newFileModeBtn.addEventListener('click', () => {
|
||||
if (mergeState.activeMode === 'file') return;
|
||||
newFileModeBtn.addEventListener('click', () => {
|
||||
if (mergeState.activeMode === 'file') return;
|
||||
|
||||
mergeState.activeMode = 'file';
|
||||
filePanel.classList.remove('hidden');
|
||||
pagePanel.classList.add('hidden');
|
||||
mergeState.activeMode = 'file';
|
||||
filePanel.classList.remove('hidden');
|
||||
pagePanel.classList.add('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
});
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
});
|
||||
|
||||
newPageModeBtn.addEventListener('click', async () => {
|
||||
if (mergeState.activeMode === 'page') return;
|
||||
newPageModeBtn.addEventListener('click', async () => {
|
||||
if (mergeState.activeMode === 'page') return;
|
||||
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
});
|
||||
await renderPageMergeThumbnails();
|
||||
});
|
||||
|
||||
if (wasInPageMode) {
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
if (wasInPageMode) {
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
}
|
||||
}
|
||||
await renderPageMergeThumbnails();
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,106 +5,124 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupNUpUI() {
|
||||
const addBorderCheckbox = document.getElementById('add-border');
|
||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||
if (addBorderCheckbox && borderColorWrapper) {
|
||||
addBorderCheckbox.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
||||
});
|
||||
}
|
||||
const addBorderCheckbox = document.getElementById('add-border');
|
||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||
if (addBorderCheckbox && borderColorWrapper) {
|
||||
addBorderCheckbox.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function nUpTool() {
|
||||
// 1. Gather all options from the UI
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('output-page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
let orientation = document.getElementById('output-orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const useMargins = document.getElementById('add-margins').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addBorder = document.getElementById('add-border').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const borderColor = hexToRgb(document.getElementById('border-color').value);
|
||||
|
||||
showLoader('Creating N-Up PDF...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
// 1. Gather all options from the UI
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('output-page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
let orientation = document.getElementById('output-orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const useMargins = document.getElementById('add-margins').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addBorder = document.getElementById('add-border').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const borderColor = hexToRgb(document.getElementById('border-color').value);
|
||||
|
||||
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
||||
|
||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||
showLoader('Creating N-Up PDF...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
if (orientation === 'auto') {
|
||||
const firstPage = sourcePages[0];
|
||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
|
||||
}
|
||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
||||
|
||||
const margin = useMargins ? 36 : 0;
|
||||
const gutter = useMargins ? 10 : 0;
|
||||
|
||||
const usableWidth = pageWidth - (margin * 2);
|
||||
const usableHeight = pageHeight - (margin * 2);
|
||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||
|
||||
// Loop through the source pages in chunks of 'n'
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
// Calculate dimensions of each cell in the grid
|
||||
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
|
||||
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
|
||||
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const sourcePage = chunk[j];
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
|
||||
const scaledWidth = embeddedPage.width * scale;
|
||||
const scaledHeight = embeddedPage.height * scale;
|
||||
|
||||
// Calculate position (x, y) for this cell
|
||||
const row = Math.floor(j / gridDims[0]);
|
||||
const col = j % gridDims[0];
|
||||
const cellX = margin + col * (cellWidth + gutter);
|
||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||
|
||||
// Center the page within its cell
|
||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
||||
|
||||
if (addBorder) {
|
||||
outputPage.drawRectangle({
|
||||
x, y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (orientation === 'auto') {
|
||||
const firstPage = sourcePages[0];
|
||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||
orientation =
|
||||
isSourceLandscape && gridDims[0] > gridDims[1]
|
||||
? 'landscape'
|
||||
: 'portrait';
|
||||
}
|
||||
}
|
||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
const margin = useMargins ? 36 : 0;
|
||||
const gutter = useMargins ? 10 : 0;
|
||||
|
||||
const usableWidth = pageWidth - margin * 2;
|
||||
const usableHeight = pageHeight - margin * 2;
|
||||
|
||||
// Loop through the source pages in chunks of 'n'
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
// Calculate dimensions of each cell in the grid
|
||||
const cellWidth =
|
||||
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
|
||||
const cellHeight =
|
||||
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
|
||||
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const sourcePage = chunk[j];
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||
const scale = Math.min(
|
||||
cellWidth / embeddedPage.width,
|
||||
cellHeight / embeddedPage.height
|
||||
);
|
||||
const scaledWidth = embeddedPage.width * scale;
|
||||
const scaledHeight = embeddedPage.height * scale;
|
||||
|
||||
// Calculate position (x, y) for this cell
|
||||
const row = Math.floor(j / gridDims[0]);
|
||||
const col = j % gridDims[0];
|
||||
const cellX = margin + col * (cellWidth + gutter);
|
||||
const cellY =
|
||||
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||
|
||||
// Center the page within its cell
|
||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
|
||||
if (addBorder) {
|
||||
outputPage.drawRectangle({
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`n-up_${n}.pdf`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,275 +4,307 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import { icons, createIcons } from "lucide";
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
let searchablePdfBytes: any = null;
|
||||
|
||||
|
||||
function sanitizeTextForWinAnsi(text: string): string {
|
||||
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||
return text
|
||||
.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||
}
|
||||
|
||||
function parseHOCR(hocrText: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||
const words = [];
|
||||
|
||||
// Find all word elements in hOCR
|
||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||
|
||||
wordElements.forEach((wordEl) => {
|
||||
const titleAttr = wordEl.getAttribute('title');
|
||||
const text = wordEl.textContent?.trim() || '';
|
||||
|
||||
if (!titleAttr || !text) return;
|
||||
|
||||
// Parse bbox coordinates from title attribute
|
||||
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||
|
||||
if (bboxMatch) {
|
||||
words.push({
|
||||
text: text,
|
||||
bbox: {
|
||||
x0: parseInt(bboxMatch[1]),
|
||||
y0: parseInt(bboxMatch[2]),
|
||||
x1: parseInt(bboxMatch[3]),
|
||||
y1: parseInt(bboxMatch[4])
|
||||
},
|
||||
confidence: confMatch ? parseInt(confMatch[1]) : 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return words;
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||
const words = [];
|
||||
|
||||
// Find all word elements in hOCR
|
||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||
|
||||
wordElements.forEach((wordEl) => {
|
||||
const titleAttr = wordEl.getAttribute('title');
|
||||
const text = wordEl.textContent?.trim() || '';
|
||||
|
||||
if (!titleAttr || !text) return;
|
||||
|
||||
// Parse bbox coordinates from title attribute
|
||||
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||
|
||||
if (bboxMatch) {
|
||||
words.push({
|
||||
text: text,
|
||||
bbox: {
|
||||
x0: parseInt(bboxMatch[1]),
|
||||
y0: parseInt(bboxMatch[2]),
|
||||
x1: parseInt(bboxMatch[3]),
|
||||
y1: parseInt(bboxMatch[4]),
|
||||
},
|
||||
confidence: confMatch ? parseInt(confMatch[1]) : 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
function binarizeCanvas(ctx: any) {
|
||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// A simple luminance-based threshold for determining black or white
|
||||
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||
data[i] = data[i + 1] = data[i + 2] = color;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// A simple luminance-based threshold for determining black or white
|
||||
const brightness =
|
||||
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||
data[i] = data[i + 1] = data[i + 2] = color;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function updateProgress(status: any, progress: any) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressStatus = document.getElementById('progress-status');
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressStatus = document.getElementById('progress-status');
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
|
||||
if (!progressBar || !progressStatus || !progressLog) return;
|
||||
if (!progressBar || !progressStatus || !progressLog) return;
|
||||
|
||||
progressStatus.textContent = status;
|
||||
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||
progressStatus.textContent = status;
|
||||
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||
|
||||
const logMessage = `Status: ${status}`;
|
||||
progressLog.textContent += logMessage + '\n';
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
const logMessage = `Status: ${status}`;
|
||||
progressLog.textContent += logMessage + '\n';
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
}
|
||||
|
||||
|
||||
async function runOCR() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const binarize = document.getElementById('ocr-binarize').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const whitelist = document.getElementById('ocr-whitelist').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const selectedLangs = Array.from(
|
||||
document.querySelectorAll('.lang-checkbox:checked')
|
||||
).map((cb) => cb.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const binarize = document.getElementById('ocr-binarize').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const whitelist = document.getElementById('ocr-whitelist').value;
|
||||
|
||||
if (selectedLangs.length === 0) {
|
||||
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
|
||||
return;
|
||||
if (selectedLangs.length === 0) {
|
||||
showAlert(
|
||||
'No Languages Selected',
|
||||
'Please select at least one language for OCR.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const langString = selectedLangs.join('+');
|
||||
|
||||
document.getElementById('ocr-options').classList.add('hidden');
|
||||
document.getElementById('ocr-progress').classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker(langString, 1, {
|
||||
logger: (m: any) => updateProgress(m.status, m.progress || 0),
|
||||
});
|
||||
|
||||
// Enable hOCR output
|
||||
await worker.setParameters({
|
||||
tessjs_create_hocr: '1',
|
||||
});
|
||||
|
||||
if (whitelist.trim()) {
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist.trim(),
|
||||
});
|
||||
}
|
||||
const langString = selectedLangs.join('+');
|
||||
|
||||
document.getElementById('ocr-options').classList.add('hidden');
|
||||
document.getElementById('ocr-progress').classList.remove('hidden');
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
let fullText = '';
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker(langString, 1, {
|
||||
logger: (m: any) => updateProgress(m.status, m.progress || 0)
|
||||
});
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
updateProgress(
|
||||
`Processing page ${i} of ${pdf.numPages}`,
|
||||
(i - 1) / pdf.numPages
|
||||
);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
// Enable hOCR output
|
||||
await worker.setParameters({
|
||||
tessjs_create_hocr: '1',
|
||||
});
|
||||
if (binarize) {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
|
||||
if (whitelist.trim()) {
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist.trim(),
|
||||
const result = await worker.recognize(canvas);
|
||||
const data = result.data;
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
newPage.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
|
||||
// Parse hOCR to get word-level data
|
||||
if (data.hocr) {
|
||||
const words = parseHOCR(data.hocr);
|
||||
|
||||
words.forEach((word: any) => {
|
||||
const { x0, y0, x1, y1 } = word.bbox;
|
||||
// Sanitize the text to remove characters WinAnsi cannot encode
|
||||
const text = sanitizeTextForWinAnsi(word.text);
|
||||
|
||||
// Skip words that become empty after sanitization
|
||||
if (!text.trim()) return;
|
||||
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
|
||||
let fontSize = bboxHeight * 0.9;
|
||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
while (textWidth > bboxWidth && fontSize > 1) {
|
||||
fontSize -= 0.5;
|
||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
}
|
||||
|
||||
try {
|
||||
newPage.drawText(text, {
|
||||
x: x0,
|
||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(0, 0, 0),
|
||||
opacity: 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// If drawing fails despite sanitization, log and skip this word
|
||||
console.warn(`Could not draw text "${text}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
let fullText = '';
|
||||
fullText += data.text + '\n\n';
|
||||
}
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
await worker.terminate();
|
||||
|
||||
if (binarize) {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
searchablePdfBytes = await newPdfDoc.save();
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
document.getElementById('ocr-results').classList.remove('hidden');
|
||||
|
||||
const result = await worker.recognize(canvas);
|
||||
const data = result.data;
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
createIcons({ icons });
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||
|
||||
// Parse hOCR to get word-level data
|
||||
if (data.hocr) {
|
||||
const words = parseHOCR(data.hocr);
|
||||
|
||||
words.forEach((word: any) => {
|
||||
const { x0, y0, x1, y1 } = word.bbox;
|
||||
// Sanitize the text to remove characters WinAnsi cannot encode
|
||||
const text = sanitizeTextForWinAnsi(word.text);
|
||||
document
|
||||
.getElementById('download-searchable-pdf')
|
||||
.addEventListener('click', () => {
|
||||
downloadFile(
|
||||
new Blob([searchablePdfBytes], { type: 'application/pdf' }),
|
||||
'searchable.pdf'
|
||||
);
|
||||
});
|
||||
|
||||
// Skip words that become empty after sanitization
|
||||
if (!text.trim()) return;
|
||||
// CHANGE: The copy button logic is updated to be safer.
|
||||
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
|
||||
const textToCopy = document.getElementById('ocr-text-output').value;
|
||||
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
button.textContent = ''; // Clear the button safely
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check');
|
||||
icon.className = 'w-4 h-4 text-green-400';
|
||||
button.appendChild(icon);
|
||||
createIcons({ icons });
|
||||
|
||||
let fontSize = bboxHeight * 0.9;
|
||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
while (textWidth > bboxWidth && fontSize > 1) {
|
||||
fontSize -= 0.5;
|
||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
}
|
||||
setTimeout(() => {
|
||||
const currentButton = document.getElementById('copy-text-btn');
|
||||
if (currentButton) {
|
||||
currentButton.textContent = ''; // Clear the button safely
|
||||
const resetIcon = document.createElement('i');
|
||||
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
||||
resetIcon.className = 'w-4 h-4 text-gray-300';
|
||||
currentButton.appendChild(resetIcon);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
newPage.drawText(text, {
|
||||
x: x0,
|
||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(0, 0, 0),
|
||||
opacity: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
// If drawing fails despite sanitization, log and skip this word
|
||||
console.warn(`Could not draw text "${text}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fullText += data.text + '\n\n';
|
||||
}
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
searchablePdfBytes = await newPdfDoc.save();
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
document.getElementById('ocr-results').classList.remove('hidden');
|
||||
|
||||
createIcons({icons});
|
||||
document
|
||||
.getElementById('download-txt-btn')
|
||||
.addEventListener('click', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||
|
||||
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
|
||||
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
|
||||
});
|
||||
|
||||
// CHANGE: The copy button logic is updated to be safer.
|
||||
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
|
||||
const textToCopy = document.getElementById('ocr-text-output').value;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
button.textContent = ''; // Clear the button safely
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check');
|
||||
icon.className = 'w-4 h-4 text-green-400';
|
||||
button.appendChild(icon);
|
||||
createIcons({ icons });
|
||||
|
||||
setTimeout(() => {
|
||||
const currentButton = document.getElementById('copy-text-btn');
|
||||
if (currentButton) {
|
||||
currentButton.textContent = ''; // Clear the button safely
|
||||
const resetIcon = document.createElement('i');
|
||||
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
||||
resetIcon.className = 'w-4 h-4 text-gray-300';
|
||||
currentButton.appendChild(resetIcon);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('download-txt-btn').addEventListener('click', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const textToSave = document.getElementById('ocr-text-output').value;
|
||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||
downloadFile(blob, 'ocr-text.txt');
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
||||
document.getElementById('ocr-options').classList.remove('hidden');
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
}
|
||||
const textToSave = document.getElementById('ocr-text-output').value;
|
||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||
downloadFile(blob, 'ocr-text.txt');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'OCR Error',
|
||||
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
|
||||
);
|
||||
document.getElementById('ocr-options').classList.remove('hidden');
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up the UI and event listeners for the OCR tool.
|
||||
*/
|
||||
export function setupOcrTool() {
|
||||
const langSearch = document.getElementById('lang-search');
|
||||
const langList = document.getElementById('lang-list');
|
||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const langSearch = document.getElementById('lang-search');
|
||||
const langList = document.getElementById('lang-list');
|
||||
const selectedLangsDisplay = document.getElementById(
|
||||
'selected-langs-display'
|
||||
);
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
langSearch.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach(label => {
|
||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
langSearch.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach((label) => {
|
||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||
? ''
|
||||
: 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Update the display of selected languages
|
||||
langList.addEventListener('change', () => {
|
||||
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
.map(cb => tesseractLanguages[cb.value]);
|
||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = selected.length === 0;
|
||||
});
|
||||
// Update the display of selected languages
|
||||
langList.addEventListener('change', () => {
|
||||
const selected = Array.from(
|
||||
langList.querySelectorAll('.lang-checkbox:checked')
|
||||
)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
.map((cb) => tesseractLanguages[cb.value]);
|
||||
selectedLangsDisplay.textContent =
|
||||
selected.length > 0 ? selected.join(', ') : 'None';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = selected.length === 0;
|
||||
});
|
||||
|
||||
// Attach the main OCR function to the process button
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
// Attach the main OCR function to the process button
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,22 +5,27 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function organize() {
|
||||
showLoader('Saving changes...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageContainer = document.getElementById('page-organizer');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndices = Array.from(pageContainer.children).map(child => parseInt(child.dataset.pageIndex));
|
||||
showLoader('Saving changes...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageContainer = document.getElementById('page-organizer');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndices = Array.from(pageContainer.children).map((child) =>
|
||||
parseInt(child.dataset.pageIndex)
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not save the changes.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'organized.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not save the changes.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,37 +8,37 @@ let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
|
||||
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
|
||||
*/
|
||||
function renderTable(unit: any) {
|
||||
const tableBody = document.getElementById('dimensions-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.textContent = ''; // Clear the table body safely
|
||||
const tableBody = document.getElementById('dimensions-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
analyzedPagesData.forEach(pageData => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
tableBody.textContent = ''; // Clear the table body safely
|
||||
|
||||
// Create and append each cell safely using textContent
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
sizeCell.textContent = pageData.standardSize;
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
|
||||
const orientationCell = document.createElement('td');
|
||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||
orientationCell.textContent = pageData.orientation;
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
// Create and append each cell safely using textContent
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
sizeCell.textContent = pageData.standardSize;
|
||||
|
||||
const orientationCell = document.createElement('td');
|
||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||
orientationCell.textContent = pageData.orientation;
|
||||
|
||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,35 +46,35 @@ function renderTable(unit: any) {
|
||||
* This is called once after the file is loaded.
|
||||
*/
|
||||
export function analyzeAndDisplayDimensions() {
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
analyzedPagesData = []; // Reset stored data
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
pages.forEach((page: any, index: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
analyzedPagesData.push({
|
||||
pageNum: index + 1,
|
||||
width, // Store raw width in points
|
||||
height, // Store raw height in points
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
});
|
||||
});
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
const unitsSelect = document.getElementById('units-select');
|
||||
|
||||
// Initial render with default unit (points)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
renderTable(unitsSelect.value);
|
||||
|
||||
// Show the results table
|
||||
resultsContainer.classList.remove('hidden');
|
||||
analyzedPagesData = []; // Reset stored data
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
// Add event listener to handle unit changes
|
||||
unitsSelect.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
renderTable(e.target.value);
|
||||
pages.forEach((page: any, index: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
analyzedPagesData.push({
|
||||
pageNum: index + 1,
|
||||
width, // Store raw width in points
|
||||
height, // Store raw height in points
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
const unitsSelect = document.getElementById('units-select');
|
||||
|
||||
// Initial render with default unit (points)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
renderTable(unitsSelect.value);
|
||||
|
||||
// Show the results table
|
||||
resultsContainer.classList.remove('hidden');
|
||||
|
||||
// Add event listener to handle unit changes
|
||||
unitsSelect.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
renderTable(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,84 +10,87 @@ import JSZip from 'jszip';
|
||||
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
|
||||
*/
|
||||
function encodeBMP(imageData: any) {
|
||||
const { width, height, data } = imageData;
|
||||
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
||||
const fileSize = stride * height + 54; // 54 byte header
|
||||
const buffer = new ArrayBuffer(fileSize);
|
||||
const view = new DataView(buffer);
|
||||
const { width, height, data } = imageData;
|
||||
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
||||
const fileSize = stride * height + 54; // 54 byte header
|
||||
const buffer = new ArrayBuffer(fileSize);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
view.setUint16(0, 0x4D42, true); // 'BM'
|
||||
view.setUint32(2, fileSize, true);
|
||||
view.setUint32(10, 54, true); // Offset to pixel data
|
||||
// BMP File Header (14 bytes)
|
||||
view.setUint16(0, 0x4d42, true); // 'BM'
|
||||
view.setUint32(2, fileSize, true);
|
||||
view.setUint32(10, 54, true); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
||||
view.setUint32(14, 40, true); // DIB header size
|
||||
view.setUint32(18, width, true);
|
||||
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
||||
view.setUint16(26, 1, true); // Color planes
|
||||
view.setUint16(28, 24, true); // Bits per pixel
|
||||
view.setUint32(30, 0, true); // No compression
|
||||
view.setUint32(34, stride * height, true); // Image size
|
||||
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
||||
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
||||
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
||||
view.setUint32(14, 40, true); // DIB header size
|
||||
view.setUint32(18, width, true);
|
||||
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
||||
view.setUint16(26, 1, true); // Color planes
|
||||
view.setUint16(28, 24, true); // Bits per pixel
|
||||
view.setUint32(30, 0, true); // No compression
|
||||
view.setUint32(34, stride * height, true); // Image size
|
||||
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
||||
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
||||
|
||||
// Pixel Data
|
||||
let offset = 54;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
// BMP is BGR, not RGB
|
||||
view.setUint8(offset++, data[i + 2]); // Blue
|
||||
view.setUint8(offset++, data[i + 1]); // Green
|
||||
view.setUint8(offset++, data[i]); // Red
|
||||
}
|
||||
// Add padding to make the row a multiple of 4 bytes
|
||||
for (let p = 0; p < (stride - width * 3); p++) {
|
||||
view.setUint8(offset++, 0);
|
||||
}
|
||||
// Pixel Data
|
||||
let offset = 54;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
// BMP is BGR, not RGB
|
||||
view.setUint8(offset++, data[i + 2]); // Blue
|
||||
view.setUint8(offset++, data[i + 1]); // Green
|
||||
view.setUint8(offset++, data[i]); // Red
|
||||
}
|
||||
return buffer;
|
||||
// Add padding to make the row a multiple of 4 bytes
|
||||
for (let p = 0; p < stride - width * 3; p++) {
|
||||
view.setUint8(offset++, 0);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
export async function pdfToBmp() {
|
||||
showLoader('Converting PDF to BMP images...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
showLoader('Converting PDF to BMP images...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// Render the PDF page directly to the canvas
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
// Render the PDF page directly to the canvas
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
// Get the raw pixel data from this canvas
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Use our new self-contained function to create the BMP file
|
||||
const bmpBuffer = encodeBMP(imageData);
|
||||
|
||||
// Add the generated BMP file to the zip archive
|
||||
zip.file(`page_${i}.bmp`, bmpBuffer);
|
||||
}
|
||||
// Get the raw pixel data from this canvas
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||
// Use our new self-contained function to create the BMP file
|
||||
const bmpBuffer = encodeBMP(imageData);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
// Add the generated BMP file to the zip archive
|
||||
zip.file(`page_${i}.bmp`, bmpBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,54 +5,64 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function pdfToGreyscale() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to greyscale...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
||||
data[j] = avg; // red
|
||||
data[j + 1] = avg; // green
|
||||
data[j + 2] = avg; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const imageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
showLoader('Converting to greyscale...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
||||
data[j] = avg; // red
|
||||
data[j + 1] = avg; // green
|
||||
data[j + 2] = avg; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not convert to greyscale.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'greyscale.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not convert to greyscale.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,39 @@ import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function pdfToJpg() {
|
||||
showLoader('Converting to JPG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
showLoader('Converting to JPG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9));
|
||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', 0.9)
|
||||
);
|
||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'converted_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function pdfToMarkdown() {
|
||||
showLoader('Converting to Markdown...');
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
let markdown = '';
|
||||
showLoader('Converting to Markdown...');
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
let markdown = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
||||
const text = content.items.map((item: any) => item.str).join(' ');
|
||||
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
||||
}
|
||||
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
||||
const text = content.items.map((item: any) => item.str).join(' ');
|
||||
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Conversion Error',
|
||||
'Failed to convert PDF. It may be image-based or corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
|
||||
export async function pdfToPng() {
|
||||
showLoader('Converting to PNG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
zip.file(`page_${i}.png`, blob as Blob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to PNG.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
showLoader('Converting to PNG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
zip.file(`page_${i}.png`, blob as Blob);
|
||||
}
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to PNG.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,36 +3,49 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import UTIF from 'utif';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function pdfToTiff() {
|
||||
showLoader('Converting PDF to TIFF...');
|
||||
try {
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
showLoader('Converting PDF to TIFF...');
|
||||
try {
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const rgba = imageData.data;
|
||||
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
|
||||
|
||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||
}
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const rgba = imageData.data;
|
||||
const tiffBuffer = UTIF.encodeImage(
|
||||
new Uint8Array(rgba),
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function pdfToWebp() {
|
||||
showLoader('Converting to WebP...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
|
||||
zip.file(`page_${i}.webp`, blob as Blob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_webp.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to WebP.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
showLoader('Converting to WebP...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/webp', 0.9)
|
||||
);
|
||||
zip.file(`page_${i}.webp`, blob as Blob);
|
||||
}
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'converted_webp.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to WebP.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,24 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
|
||||
export async function pdfToZip() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating ZIP file...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (const file of state.files) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
zip.file(file.name, fileBuffer as ArrayBuffer);
|
||||
}
|
||||
showLoader('Creating ZIP file...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (const file of state.files) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
zip.file(file.name, fileBuffer as ArrayBuffer);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdfs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create ZIP file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdfs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create ZIP file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -6,30 +5,36 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function pngToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PNG file.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PNG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating PDF from PNGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
showLoader('Creating PDF from PNGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_pngs.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,197 +6,282 @@ import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
const posterizeState = {
|
||||
pdfJsDoc: null,
|
||||
pageSnapshots: {},
|
||||
currentPage: 1,
|
||||
pdfJsDoc: null,
|
||||
pageSnapshots: {},
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
async function renderPosterizePreview(pageNum: number) {
|
||||
if (!posterizeState.pdfJsDoc) return;
|
||||
|
||||
posterizeState.currentPage = pageNum;
|
||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||
if (!posterizeState.pdfJsDoc) return;
|
||||
|
||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
posterizeState.currentPage = pageNum;
|
||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||
|
||||
if (posterizeState.pageSnapshots[pageNum]) {
|
||||
canvas.width = posterizeState.pageSnapshots[pageNum].width;
|
||||
canvas.height = posterizeState.pageSnapshots[pageNum].height;
|
||||
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
|
||||
} else {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
updatePreviewNav();
|
||||
drawGridOverlay();
|
||||
hideLoader();
|
||||
const canvas = document.getElementById(
|
||||
'posterize-preview-canvas'
|
||||
) as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (posterizeState.pageSnapshots[pageNum]) {
|
||||
canvas.width = posterizeState.pageSnapshots[pageNum].width;
|
||||
canvas.height = posterizeState.pageSnapshots[pageNum].height;
|
||||
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
|
||||
} else {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
posterizeState.pageSnapshots[pageNum] = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
}
|
||||
|
||||
updatePreviewNav();
|
||||
drawGridOverlay();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function drawGridOverlay() {
|
||||
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||
|
||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0);
|
||||
const canvas = document.getElementById(
|
||||
'posterize-preview-canvas'
|
||||
) as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
context.putImageData(
|
||||
posterizeState.pageSnapshots[posterizeState.currentPage],
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
||||
const pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages);
|
||||
|
||||
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
||||
const pageRangeInput = (
|
||||
document.getElementById('page-range') as HTMLInputElement
|
||||
).value;
|
||||
const pagesToProcess = parsePageRanges(
|
||||
pageRangeInput,
|
||||
posterizeState.pdfJsDoc.numPages
|
||||
);
|
||||
|
||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||
context.lineWidth = 2;
|
||||
context.setLineDash([10, 5]);
|
||||
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
||||
const rows =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const cols =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||
) || 1;
|
||||
|
||||
const cellWidth = canvas.width / cols;
|
||||
const cellHeight = canvas.height / rows;
|
||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||
context.lineWidth = 2;
|
||||
context.setLineDash([10, 5]);
|
||||
|
||||
for (let i = 1; i < cols; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(i * cellWidth, 0);
|
||||
context.lineTo(i * cellWidth, canvas.height);
|
||||
context.stroke();
|
||||
}
|
||||
const cellWidth = canvas.width / cols;
|
||||
const cellHeight = canvas.height / rows;
|
||||
|
||||
for (let i = 1; i < rows; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, i * cellHeight);
|
||||
context.lineTo(canvas.width, i * cellHeight);
|
||||
context.stroke();
|
||||
}
|
||||
context.setLineDash([]);
|
||||
for (let i = 1; i < cols; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(i * cellWidth, 0);
|
||||
context.lineTo(i * cellWidth, canvas.height);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (let i = 1; i < rows; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, i * cellHeight);
|
||||
context.lineTo(canvas.width, i * cellHeight);
|
||||
context.stroke();
|
||||
}
|
||||
context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewNav() {
|
||||
const currentPageSpan = document.getElementById('current-preview-page');
|
||||
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
||||
const currentPageSpan = document.getElementById('current-preview-page');
|
||||
const prevBtn = document.getElementById(
|
||||
'prev-preview-page'
|
||||
) as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById(
|
||||
'next-preview-page'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||
nextBtn.disabled =
|
||||
posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||
}
|
||||
|
||||
export async function setupPosterizeTool() {
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
posterizeState.pageSnapshots = {};
|
||||
posterizeState.currentPage = 1;
|
||||
|
||||
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
|
||||
await renderPosterizePreview(1);
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||
.getPageCount()
|
||||
.toString();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
|
||||
.promise;
|
||||
posterizeState.pageSnapshots = {};
|
||||
posterizeState.currentPage = 1;
|
||||
|
||||
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
|
||||
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
|
||||
document.getElementById('total-preview-pages').textContent =
|
||||
posterizeState.pdfJsDoc.numPages.toString();
|
||||
await renderPosterizePreview(1);
|
||||
|
||||
['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
||||
});
|
||||
createIcons({ icons });
|
||||
}
|
||||
document.getElementById('prev-preview-page').onclick = () =>
|
||||
renderPosterizePreview(posterizeState.currentPage - 1);
|
||||
document.getElementById('next-preview-page').onclick = () =>
|
||||
renderPosterizePreview(posterizeState.currentPage + 1);
|
||||
|
||||
['posterize-rows', 'posterize-cols', 'page-range'].forEach((id) => {
|
||||
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
||||
});
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
|
||||
export async function posterize() {
|
||||
showLoader('Posterizing PDF...');
|
||||
try {
|
||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value;
|
||||
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
||||
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
||||
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
||||
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
||||
showLoader('Posterizing PDF...');
|
||||
try {
|
||||
const rows =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const cols =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const pageSizeKey = (
|
||||
document.getElementById('output-page-size') as HTMLSelectElement
|
||||
).value;
|
||||
let orientation = (
|
||||
document.getElementById('output-orientation') as HTMLSelectElement
|
||||
).value;
|
||||
const scalingMode = (
|
||||
document.querySelector(
|
||||
'input[name="scaling-mode"]:checked'
|
||||
) as HTMLInputElement
|
||||
).value;
|
||||
const overlap =
|
||||
parseFloat(
|
||||
(document.getElementById('overlap') as HTMLInputElement).value
|
||||
) || 0;
|
||||
const overlapUnits = (
|
||||
document.getElementById('overlap-units') as HTMLSelectElement
|
||||
).value;
|
||||
const pageRangeInput = (
|
||||
document.getElementById('page-range') as HTMLInputElement
|
||||
).value;
|
||||
|
||||
let overlapInPoints = overlap;
|
||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||
let overlapInPoints = overlap;
|
||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const totalPages = posterizeState.pdfJsDoc.numPages;
|
||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
const newDoc = await PDFDocument.create();
|
||||
const totalPages = posterizeState.pdfJsDoc.numPages;
|
||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
|
||||
if (pageIndicesToProcess.length === 0) {
|
||||
throw new Error("Invalid page range specified.");
|
||||
}
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
|
||||
for (const pageIndex of pageIndicesToProcess) {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport }).promise;
|
||||
|
||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
||||
let currentOrientation = orientation;
|
||||
|
||||
if (currentOrientation === 'auto') {
|
||||
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const tileWidth = tempCanvas.width / cols;
|
||||
const tileHeight = tempCanvas.height / rows;
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
|
||||
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
|
||||
|
||||
const tileCanvas = document.createElement('canvas');
|
||||
tileCanvas.width = sWidth;
|
||||
tileCanvas.height = sHeight;
|
||||
tileCanvas.getContext('2d').drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
||||
|
||||
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
|
||||
const scaleX = newPage.getWidth() / sWidth;
|
||||
const scaleY = newPage.getHeight() / sHeight;
|
||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sWidth * scale;
|
||||
const scaledHeight = sHeight * scale;
|
||||
|
||||
newPage.drawImage(tileImage, {
|
||||
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'posterized.pdf');
|
||||
showAlert('Success', 'Your PDF has been posterized.');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (pageIndicesToProcess.length === 0) {
|
||||
throw new Error('Invalid page range specified.');
|
||||
}
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
|
||||
for (const pageIndex of pageIndicesToProcess) {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport }).promise;
|
||||
|
||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
||||
let currentOrientation = orientation;
|
||||
|
||||
if (currentOrientation === 'auto') {
|
||||
currentOrientation =
|
||||
viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (
|
||||
currentOrientation === 'portrait' &&
|
||||
targetWidth > targetHeight
|
||||
) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const tileWidth = tempCanvas.width / cols;
|
||||
const tileHeight = tempCanvas.height / rows;
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||
const sWidth =
|
||||
tileWidth +
|
||||
(c > 0 ? overlapInPoints : 0) +
|
||||
(c < cols - 1 ? overlapInPoints : 0);
|
||||
const sHeight =
|
||||
tileHeight +
|
||||
(r > 0 ? overlapInPoints : 0) +
|
||||
(r < rows - 1 ? overlapInPoints : 0);
|
||||
|
||||
const tileCanvas = document.createElement('canvas');
|
||||
tileCanvas.width = sWidth;
|
||||
tileCanvas.height = sHeight;
|
||||
tileCanvas
|
||||
.getContext('2d')
|
||||
.drawImage(
|
||||
tempCanvas,
|
||||
sx,
|
||||
sy,
|
||||
sWidth,
|
||||
sHeight,
|
||||
0,
|
||||
0,
|
||||
sWidth,
|
||||
sHeight
|
||||
);
|
||||
|
||||
const tileImage = await newDoc.embedPng(
|
||||
tileCanvas.toDataURL('image/png')
|
||||
);
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
|
||||
const scaleX = newPage.getWidth() / sWidth;
|
||||
const scaleY = newPage.getHeight() / sHeight;
|
||||
const scale =
|
||||
scalingMode === 'fit'
|
||||
? Math.min(scaleX, scaleY)
|
||||
: Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sWidth * scale;
|
||||
const scaledHeight = sHeight * scale;
|
||||
|
||||
newPage.drawImage(tileImage, {
|
||||
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'posterized.pdf'
|
||||
);
|
||||
showAlert('Success', 'Your PDF has been posterized.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,36 +6,39 @@ import { state } from '../state.js';
|
||||
const { rgb } = window.PDFLib;
|
||||
|
||||
export async function redact(redactions: any, canvasScale: any) {
|
||||
showLoader('Applying redactions...');
|
||||
try {
|
||||
const pdfPages = state.pdfDoc.getPages();
|
||||
const conversionScale = 1 / canvasScale;
|
||||
showLoader('Applying redactions...');
|
||||
try {
|
||||
const pdfPages = state.pdfDoc.getPages();
|
||||
const conversionScale = 1 / canvasScale;
|
||||
|
||||
redactions.forEach((r: any) => {
|
||||
const page = pdfPages[r.pageIndex];
|
||||
const { height: pageHeight } = page.getSize();
|
||||
redactions.forEach((r: any) => {
|
||||
const page = pdfPages[r.pageIndex];
|
||||
const { height: pageHeight } = page.getSize();
|
||||
|
||||
// Convert canvas coordinates back to PDF coordinates
|
||||
const pdfX = r.canvasX * conversionScale;
|
||||
const pdfWidth = r.canvasWidth * conversionScale;
|
||||
const pdfHeight = r.canvasHeight * conversionScale;
|
||||
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
|
||||
// Convert canvas coordinates back to PDF coordinates
|
||||
const pdfX = r.canvasX * conversionScale;
|
||||
const pdfWidth = r.canvasWidth * conversionScale;
|
||||
const pdfHeight = r.canvasHeight * conversionScale;
|
||||
const pdfY = pageHeight - r.canvasY * conversionScale - pdfHeight;
|
||||
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: pdfWidth,
|
||||
height: pdfHeight,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
});
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: pdfWidth,
|
||||
height: pdfHeight,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
});
|
||||
|
||||
const redactedBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply redactions.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const redactedBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([redactedBytes], { type: 'application/pdf' }),
|
||||
'redacted.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply redactions.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,101 +4,123 @@ import { state } from '../state.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export function setupRemoveAnnotationsTool() {
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent =
|
||||
state.pdfDoc.getPageCount();
|
||||
}
|
||||
|
||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
pageScopeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||
});
|
||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
pageScopeRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||
});
|
||||
});
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||
selectAllCheckbox.addEventListener('change', () => {
|
||||
allAnnotCheckboxes.forEach(checkbox => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||
selectAllCheckbox.addEventListener('change', () => {
|
||||
allAnnotCheckboxes.forEach((checkbox) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeAnnotations() {
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let targetPageIndices = [];
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let targetPageIndices = [];
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const pageScope = document.querySelector('input[name="page-scope"]:checked').value;
|
||||
if (pageScope === 'all') {
|
||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const pageScope = document.querySelector(
|
||||
'input[name="page-scope"]:checked'
|
||||
).value;
|
||||
if (pageScope === 'all') {
|
||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInput = document.getElementById('page-range-input').value;
|
||||
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
||||
const ranges = rangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInput = document.getElementById('page-range-input').value;
|
||||
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
||||
const ranges = rangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
targetPageIndices.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
targetPageIndices = [...new Set(targetPageIndices)];
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
targetPageIndices.push(pageNum - 1);
|
||||
}
|
||||
|
||||
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
|
||||
|
||||
const typesToRemove = new Set(
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(cb => cb.value)
|
||||
);
|
||||
|
||||
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (const pageIndex of targetPageIndices) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = state.pdfDoc.context.lookup(ref);
|
||||
const subtype = annot.get(PDFName.of('Subtype'))?.toString().substring(1);
|
||||
|
||||
// If the subtype is NOT in the list to remove, add it to our new array
|
||||
if (!subtype || !typesToRemove.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
targetPageIndices = [...new Set(targetPageIndices)];
|
||||
}
|
||||
|
||||
if (targetPageIndices.length === 0)
|
||||
throw new Error('No valid pages were selected.');
|
||||
|
||||
const typesToRemove = new Set(
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
|
||||
(cb) => cb.value
|
||||
)
|
||||
);
|
||||
|
||||
if (typesToRemove.size === 0)
|
||||
throw new Error('Please select at least one annotation type to remove.');
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (const pageIndex of targetPageIndices) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = state.pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
// If the subtype is NOT in the list to remove, add it to our new array
|
||||
if (!subtype || !typesToRemove.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'annotations-removed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
e.message || 'Could not remove annotations. Please check your page range.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,147 +8,167 @@ import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
|
||||
let analysisCache = [];
|
||||
|
||||
async function isPageBlank(page: PDFPageProxy, threshold: number) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
const viewport = page.getViewport({ scale: 0.2 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
|
||||
const viewport = page.getViewport({ scale: 0.2 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const totalPixels = data.length / 4;
|
||||
let nonWhitePixels = 0;
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||
.promise;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] < 245 || data[i+1] < 245 || data[i+2] < 245) {
|
||||
nonWhitePixels++;
|
||||
}
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const totalPixels = data.length / 4;
|
||||
let nonWhitePixels = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) {
|
||||
nonWhitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
const blankness = 1 - (nonWhitePixels / totalPixels);
|
||||
return blankness >= (threshold / 100);
|
||||
const blankness = 1 - nonWhitePixels / totalPixels;
|
||||
return blankness >= threshold / 100;
|
||||
}
|
||||
|
||||
async function analyzePages() {
|
||||
if (!state.pdfDoc) return;
|
||||
showLoader('Analyzing for blank pages...');
|
||||
if (!state.pdfDoc) return;
|
||||
showLoader('Analyzing for blank pages...');
|
||||
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
analysisCache = [];
|
||||
const promises = [];
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
promises.push(
|
||||
pdf.getPage(i).then(page =>
|
||||
isPageBlank(page, 0).then(isActuallyBlank => ({
|
||||
pageNum: i,
|
||||
isInitiallyBlank: isActuallyBlank,
|
||||
pageRef: page,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
analysisCache = await Promise.all(promises);
|
||||
hideLoader();
|
||||
updateAnalysisUI();
|
||||
analysisCache = [];
|
||||
const promises = [];
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
promises.push(
|
||||
pdf.getPage(i).then((page) =>
|
||||
isPageBlank(page, 0).then((isActuallyBlank) => ({
|
||||
pageNum: i,
|
||||
isInitiallyBlank: isActuallyBlank,
|
||||
pageRef: page,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
analysisCache = await Promise.all(promises);
|
||||
hideLoader();
|
||||
updateAnalysisUI();
|
||||
}
|
||||
|
||||
|
||||
async function updateAnalysisUI() {
|
||||
const sensitivity = parseInt((document.getElementById('sensitivity-slider') as HTMLInputElement).value);
|
||||
(document.getElementById('sensitivity-value') as HTMLSpanElement).textContent = sensitivity.toString();
|
||||
const sensitivity = parseInt(
|
||||
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||
);
|
||||
(
|
||||
document.getElementById('sensitivity-value') as HTMLSpanElement
|
||||
).textContent = sensitivity.toString();
|
||||
|
||||
const previewContainer = document.getElementById('analysis-preview');
|
||||
const analysisText = document.getElementById('analysis-text');
|
||||
const thumbnailsContainer = document.getElementById('removed-pages-thumbnails');
|
||||
|
||||
thumbnailsContainer.innerHTML = '';
|
||||
|
||||
const pagesToRemove = [];
|
||||
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||
if (isConsideredBlank) {
|
||||
pagesToRemove.push(pageData.pageNum);
|
||||
}
|
||||
const previewContainer = document.getElementById('analysis-preview');
|
||||
const analysisText = document.getElementById('analysis-text');
|
||||
const thumbnailsContainer = document.getElementById(
|
||||
'removed-pages-thumbnails'
|
||||
);
|
||||
|
||||
thumbnailsContainer.innerHTML = '';
|
||||
|
||||
const pagesToRemove = [];
|
||||
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||
if (isConsideredBlank) {
|
||||
pagesToRemove.push(pageData.pageNum);
|
||||
}
|
||||
|
||||
if (pagesToRemove.length > 0) {
|
||||
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
|
||||
previewContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
for (const pageNum of pagesToRemove) {
|
||||
const pageData = analysisCache[pageNum-1];
|
||||
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
|
||||
const thumbCanvas = document.createElement('canvas');
|
||||
thumbCanvas.width = viewport.width;
|
||||
thumbCanvas.height = viewport.height;
|
||||
await pageData.pageRef.render({ canvasContext: thumbCanvas.getContext('2d'), viewport }).promise;
|
||||
if (pagesToRemove.length > 0) {
|
||||
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
|
||||
previewContainer.classList.remove('hidden');
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = thumbCanvas.toDataURL();
|
||||
img.className = 'rounded border border-gray-600';
|
||||
img.title = `Page ${pageNum}`;
|
||||
thumbnailsContainer.appendChild(img);
|
||||
}
|
||||
for (const pageNum of pagesToRemove) {
|
||||
const pageData = analysisCache[pageNum - 1];
|
||||
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
|
||||
const thumbCanvas = document.createElement('canvas');
|
||||
thumbCanvas.width = viewport.width;
|
||||
thumbCanvas.height = viewport.height;
|
||||
await pageData.pageRef.render({
|
||||
canvasContext: thumbCanvas.getContext('2d'),
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
} else {
|
||||
analysisText.textContent = 'No blank pages found at this sensitivity level.';
|
||||
previewContainer.classList.remove('hidden');
|
||||
const img = document.createElement('img');
|
||||
img.src = thumbCanvas.toDataURL();
|
||||
img.className = 'rounded border border-gray-600';
|
||||
img.title = `Page ${pageNum}`;
|
||||
thumbnailsContainer.appendChild(img);
|
||||
}
|
||||
} else {
|
||||
analysisText.textContent =
|
||||
'No blank pages found at this sensitivity level.';
|
||||
previewContainer.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function setupRemoveBlankPagesTool() {
|
||||
await analyzePages();
|
||||
document.getElementById('sensitivity-slider').addEventListener('input', updateAnalysisUI);
|
||||
await analyzePages();
|
||||
document
|
||||
.getElementById('sensitivity-slider')
|
||||
.addEventListener('input', updateAnalysisUI);
|
||||
}
|
||||
|
||||
export async function removeBlankPages() {
|
||||
showLoader('Removing blank pages...');
|
||||
try {
|
||||
const sensitivity = parseInt((document.getElementById('sensitivity-slider') as HTMLInputElement).value);
|
||||
const indicesToKeep = [];
|
||||
showLoader('Removing blank pages...');
|
||||
try {
|
||||
const sensitivity = parseInt(
|
||||
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||
);
|
||||
const indicesToKeep = [];
|
||||
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||
if (!isConsideredBlank) {
|
||||
indicesToKeep.push(pageData.pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === 0) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'No Content Found',
|
||||
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
|
||||
hideLoader();
|
||||
showAlert('No Pages Removed', 'No pages were identified as blank at the current sensitivity level.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach(page => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'non-blank.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not remove blank pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(
|
||||
pageData.pageRef,
|
||||
sensitivity
|
||||
);
|
||||
if (!isConsideredBlank) {
|
||||
indicesToKeep.push(pageData.pageNum - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === 0) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'No Content Found',
|
||||
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'No Pages Removed',
|
||||
'No pages were identified as blank at the current sensitivity level.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'non-blank.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not remove blank pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function removeMetadata() {
|
||||
showLoader('Removing all metadata...');
|
||||
try {
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
showLoader('Removing all metadata...');
|
||||
try {
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
state.pdfDoc.setTitle('');
|
||||
state.pdfDoc.setAuthor('');
|
||||
state.pdfDoc.setSubject('');
|
||||
state.pdfDoc.setKeywords([]);
|
||||
state.pdfDoc.setCreator('');
|
||||
state.pdfDoc.setProducer('');
|
||||
state.pdfDoc.setTitle('');
|
||||
state.pdfDoc.setAuthor('');
|
||||
state.pdfDoc.setSubject('');
|
||||
state.pdfDoc.setKeywords([]);
|
||||
state.pdfDoc.setCreator('');
|
||||
state.pdfDoc.setProducer('');
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'metadata-removed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,31 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function reversePages() {
|
||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
||||
showLoader('Reversing page order...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = state.pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Reversing page order...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = state.pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
{ length: pageCount },
|
||||
(_, i) => pageCount - 1 - i
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'reversed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,29 @@ import { state } from '../state.js';
|
||||
import { degrees } from 'pdf-lib';
|
||||
|
||||
export async function rotate() {
|
||||
showLoader('Applying rotations...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(item.dataset.pageIndex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const rotation = parseInt(item.dataset.rotation || '0');
|
||||
if (rotation !== 0) {
|
||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||
}
|
||||
});
|
||||
showLoader('Applying rotations...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(item.dataset.pageIndex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const rotation = parseInt(item.dataset.rotation || '0');
|
||||
if (rotation !== 0) {
|
||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||
}
|
||||
});
|
||||
|
||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not apply rotations.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
||||
'rotated.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not apply rotations.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// This is essentially the same as image-to-pdf.
|
||||
import { imageToPdf } from './image-to-pdf.js';
|
||||
|
||||
export const scanToPdf = imageToPdf;
|
||||
export const scanToPdf = imageToPdf;
|
||||
|
||||
@@ -4,413 +4,544 @@ import { state } from '../state.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
const signState = {
|
||||
pdf: null, canvas: null, context: null, pageRendering: false,
|
||||
currentPageNum: 1, scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
drawCanvas: null, drawContext: null, isDrawing: false,
|
||||
savedSignatures: [], placedSignatures: [], activeSignature: null,
|
||||
interactionMode: 'none',
|
||||
draggedSigId: null,
|
||||
dragOffsetX: 0, dragOffsetY: 0,
|
||||
hoveredSigId: null,
|
||||
resizeHandle: null,
|
||||
pdf: null,
|
||||
canvas: null,
|
||||
context: null,
|
||||
pageRendering: false,
|
||||
currentPageNum: 1,
|
||||
scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
drawCanvas: null,
|
||||
drawContext: null,
|
||||
isDrawing: false,
|
||||
savedSignatures: [],
|
||||
placedSignatures: [],
|
||||
activeSignature: null,
|
||||
interactionMode: 'none',
|
||||
draggedSigId: null,
|
||||
dragOffsetX: 0,
|
||||
dragOffsetY: 0,
|
||||
hoveredSigId: null,
|
||||
resizeHandle: null,
|
||||
};
|
||||
|
||||
|
||||
async function renderPage(num: any) {
|
||||
signState.pageRendering = true;
|
||||
const page = await signState.pdf.getPage(num);
|
||||
const viewport = page.getViewport({ scale: signState.scale });
|
||||
signState.canvas.height = viewport.height;
|
||||
signState.canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: signState.context, viewport }).promise;
|
||||
signState.pageRendering = true;
|
||||
const page = await signState.pdf.getPage(num);
|
||||
const viewport = page.getViewport({ scale: signState.scale });
|
||||
signState.canvas.height = viewport.height;
|
||||
signState.canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: signState.context, viewport }).promise;
|
||||
|
||||
signState.pageSnapshot = signState.context.getImageData(0, 0, signState.canvas.width, signState.canvas.height);
|
||||
|
||||
drawSignatures();
|
||||
signState.pageSnapshot = signState.context.getImageData(
|
||||
0,
|
||||
0,
|
||||
signState.canvas.width,
|
||||
signState.canvas.height
|
||||
);
|
||||
|
||||
signState.pageRendering = false;
|
||||
document.getElementById('current-page-display-sign').textContent = num;
|
||||
drawSignatures();
|
||||
|
||||
signState.pageRendering = false;
|
||||
document.getElementById('current-page-display-sign').textContent = num;
|
||||
}
|
||||
|
||||
function drawSignatures() {
|
||||
if (!signState.pageSnapshot) return;
|
||||
signState.context.putImageData(signState.pageSnapshot, 0, 0);
|
||||
if (!signState.pageSnapshot) return;
|
||||
signState.context.putImageData(signState.pageSnapshot, 0, 0);
|
||||
|
||||
signState.placedSignatures
|
||||
.filter(sig => sig.pageIndex === signState.currentPageNum - 1)
|
||||
.forEach(sig => {
|
||||
signState.context.drawImage(sig.image, sig.x, sig.y, sig.width, sig.height);
|
||||
|
||||
if (signState.hoveredSigId === sig.id || signState.draggedSigId === sig.id) {
|
||||
signState.context.strokeStyle = '#4f46e5';
|
||||
signState.context.setLineDash([6, 3]);
|
||||
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
||||
signState.context.setLineDash([]);
|
||||
signState.placedSignatures
|
||||
.filter((sig) => sig.pageIndex === signState.currentPageNum - 1)
|
||||
.forEach((sig) => {
|
||||
signState.context.drawImage(
|
||||
sig.image,
|
||||
sig.x,
|
||||
sig.y,
|
||||
sig.width,
|
||||
sig.height
|
||||
);
|
||||
|
||||
drawResizeHandles(sig);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (
|
||||
signState.hoveredSigId === sig.id ||
|
||||
signState.draggedSigId === sig.id
|
||||
) {
|
||||
signState.context.strokeStyle = '#4f46e5';
|
||||
signState.context.setLineDash([6, 3]);
|
||||
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
||||
signState.context.setLineDash([]);
|
||||
|
||||
|
||||
function drawResizeHandles(sig: any) {
|
||||
const handleSize = 8;
|
||||
const halfHandle = handleSize / 2;
|
||||
const handles = getResizeHandles(sig);
|
||||
signState.context.fillStyle = '#4f46e5';
|
||||
Object.values(handles).forEach(handle => {
|
||||
signState.context.fillRect(handle.x - halfHandle, handle.y - halfHandle, handleSize, handleSize);
|
||||
drawResizeHandles(sig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function fitToWidth() {
|
||||
const page = await signState.pdf.getPage(signState.currentPageNum);
|
||||
const container = document.getElementById('canvas-container-sign');
|
||||
signState.scale = container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
||||
renderPage(signState.currentPageNum);
|
||||
function drawResizeHandles(sig: any) {
|
||||
const handleSize = 8;
|
||||
const halfHandle = handleSize / 2;
|
||||
const handles = getResizeHandles(sig);
|
||||
signState.context.fillStyle = '#4f46e5';
|
||||
Object.values(handles).forEach((handle) => {
|
||||
signState.context.fillRect(
|
||||
handle.x - halfHandle,
|
||||
handle.y - halfHandle,
|
||||
handleSize,
|
||||
handleSize
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function fitToWidth() {
|
||||
const page = await signState.pdf.getPage(signState.currentPageNum);
|
||||
const container = document.getElementById('canvas-container-sign');
|
||||
signState.scale =
|
||||
container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
||||
renderPage(signState.currentPageNum);
|
||||
}
|
||||
|
||||
function setupDrawingCanvas() {
|
||||
signState.drawCanvas = document.getElementById('signature-draw-canvas');
|
||||
signState.drawContext = signState.drawCanvas.getContext('2d');
|
||||
|
||||
const rect = signState.drawCanvas.getBoundingClientRect();
|
||||
const dpi = window.devicePixelRatio || 1;
|
||||
signState.drawCanvas.width = rect.width * dpi;
|
||||
signState.drawCanvas.height = rect.height * dpi;
|
||||
signState.drawContext.scale(dpi, dpi);
|
||||
signState.drawContext.lineWidth = 2;
|
||||
signState.drawCanvas = document.getElementById('signature-draw-canvas');
|
||||
signState.drawContext = signState.drawCanvas.getContext('2d');
|
||||
|
||||
const colorPicker = document.getElementById('signature-color');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
colorPicker.oninput = () => signState.drawContext.strokeStyle = colorPicker.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
signState.drawContext.strokeStyle = colorPicker.value;
|
||||
const rect = signState.drawCanvas.getBoundingClientRect();
|
||||
const dpi = window.devicePixelRatio || 1;
|
||||
signState.drawCanvas.width = rect.width * dpi;
|
||||
signState.drawCanvas.height = rect.height * dpi;
|
||||
signState.drawContext.scale(dpi, dpi);
|
||||
signState.drawContext.lineWidth = 2;
|
||||
|
||||
const start = (e: any) => {
|
||||
signState.isDrawing = true;
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.beginPath();
|
||||
signState.drawContext.moveTo(pos.x, pos.y);
|
||||
};
|
||||
const draw = (e: any) => {
|
||||
if (!signState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.lineTo(pos.x, pos.y);
|
||||
signState.drawContext.stroke();
|
||||
};
|
||||
const stop = () => signState.isDrawing = false;
|
||||
|
||||
['mousedown', 'touchstart'].forEach(evt => signState.drawCanvas.addEventListener(evt, start, { passive: false }));
|
||||
['mousemove', 'touchmove'].forEach(evt => signState.drawCanvas.addEventListener(evt, draw, { passive: false }));
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => signState.drawCanvas.addEventListener(evt, stop));
|
||||
const colorPicker = document.getElementById('signature-color');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
colorPicker.oninput = () =>
|
||||
(signState.drawContext.strokeStyle = colorPicker.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
signState.drawContext.strokeStyle = colorPicker.value;
|
||||
|
||||
const start = (e: any) => {
|
||||
signState.isDrawing = true;
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.beginPath();
|
||||
signState.drawContext.moveTo(pos.x, pos.y);
|
||||
};
|
||||
const draw = (e: any) => {
|
||||
if (!signState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.lineTo(pos.x, pos.y);
|
||||
signState.drawContext.stroke();
|
||||
};
|
||||
const stop = () => (signState.isDrawing = false);
|
||||
|
||||
['mousedown', 'touchstart'].forEach((evt) =>
|
||||
signState.drawCanvas.addEventListener(evt, start, { passive: false })
|
||||
);
|
||||
['mousemove', 'touchmove'].forEach((evt) =>
|
||||
signState.drawCanvas.addEventListener(evt, draw, { passive: false })
|
||||
);
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
|
||||
signState.drawCanvas.addEventListener(evt, stop)
|
||||
);
|
||||
}
|
||||
|
||||
function getMousePos(canvas: any, evt: any) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: evt.clientX - rect.left,
|
||||
y: evt.clientY - rect.top
|
||||
};
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: evt.clientX - rect.left,
|
||||
y: evt.clientY - rect.top,
|
||||
};
|
||||
}
|
||||
|
||||
function addSignatureToSaved(imageDataUrl: any) {
|
||||
const img = new Image();
|
||||
img.src = imageDataUrl;
|
||||
signState.savedSignatures.push(img);
|
||||
renderSavedSignatures();
|
||||
const img = new Image();
|
||||
img.src = imageDataUrl;
|
||||
signState.savedSignatures.push(img);
|
||||
renderSavedSignatures();
|
||||
}
|
||||
|
||||
function renderSavedSignatures() {
|
||||
const container = document.getElementById('saved-signatures-container');
|
||||
container.textContent = ''; //change
|
||||
signState.savedSignatures.forEach((img, index) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
||||
img.className = 'h-full w-auto mx-auto';
|
||||
wrapper.appendChild(img);
|
||||
container.appendChild(wrapper);
|
||||
const container = document.getElementById('saved-signatures-container');
|
||||
container.textContent = ''; //change
|
||||
signState.savedSignatures.forEach((img, index) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
||||
img.className = 'h-full w-auto mx-auto';
|
||||
wrapper.appendChild(img);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
wrapper.onclick = () => {
|
||||
signState.activeSignature = { image: img, index };
|
||||
document.querySelectorAll('.saved-signature').forEach(el => el.classList.remove('selected'));
|
||||
wrapper.classList.add('selected');
|
||||
};
|
||||
});
|
||||
wrapper.onclick = () => {
|
||||
signState.activeSignature = { image: img, index };
|
||||
document
|
||||
.querySelectorAll('.saved-signature')
|
||||
.forEach((el) => el.classList.remove('selected'));
|
||||
wrapper.classList.add('selected');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getResizeHandles(sig: any) {
|
||||
return {
|
||||
'top-left': { x: sig.x, y: sig.y },
|
||||
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
|
||||
'top-right': { x: sig.x + sig.width, y: sig.y },
|
||||
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
|
||||
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
|
||||
'bottom-left': { x: sig.x, y: sig.y + sig.height },
|
||||
'bottom-middle':{ x: sig.x + sig.width / 2, y: sig.y + sig.height },
|
||||
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
|
||||
};
|
||||
return {
|
||||
'top-left': { x: sig.x, y: sig.y },
|
||||
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
|
||||
'top-right': { x: sig.x + sig.width, y: sig.y },
|
||||
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
|
||||
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
|
||||
'bottom-left': { x: sig.x, y: sig.y + sig.height },
|
||||
'bottom-middle': { x: sig.x + sig.width / 2, y: sig.y + sig.height },
|
||||
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getHandleAtPos(pos: any, sig: any) {
|
||||
const handles = getResizeHandles(sig);
|
||||
const handleSize = 10;
|
||||
for (const [name, handlePos] of Object.entries(handles)) {
|
||||
if (Math.abs(pos.x - handlePos.x) < handleSize && Math.abs(pos.y - handlePos.y) < handleSize) {
|
||||
return name;
|
||||
}
|
||||
const handles = getResizeHandles(sig);
|
||||
const handleSize = 10;
|
||||
for (const [name, handlePos] of Object.entries(handles)) {
|
||||
if (
|
||||
Math.abs(pos.x - handlePos.x) < handleSize &&
|
||||
Math.abs(pos.y - handlePos.y) < handleSize
|
||||
) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setupPlacementListeners() {
|
||||
const canvas = signState.canvas;
|
||||
const ghost = document.getElementById('signature-ghost');
|
||||
|
||||
const mouseMoveHandler = (e: any) => {
|
||||
if (signState.interactionMode !== 'none') return;
|
||||
const canvas = signState.canvas;
|
||||
const ghost = document.getElementById('signature-ghost');
|
||||
|
||||
if (signState.activeSignature) {
|
||||
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
|
||||
ghost.style.width = '150px';
|
||||
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
|
||||
ghost.style.left = `${e.clientX + 5}px`;
|
||||
ghost.style.top = `${e.clientY + 5}px`;
|
||||
ghost.classList.remove('hidden');
|
||||
const mouseMoveHandler = (e: any) => {
|
||||
if (signState.interactionMode !== 'none') return;
|
||||
|
||||
if (signState.activeSignature) {
|
||||
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
|
||||
ghost.style.width = '150px';
|
||||
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
|
||||
ghost.style.left = `${e.clientX + 5}px`;
|
||||
ghost.style.top = `${e.clientY + 5}px`;
|
||||
ghost.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const pos = getMousePos(canvas, e);
|
||||
let foundSigId: any = null;
|
||||
let foundHandle = null;
|
||||
|
||||
signState.placedSignatures
|
||||
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||
.reverse()
|
||||
.forEach((sig) => {
|
||||
if (foundSigId) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
foundSigId = sig.id;
|
||||
foundHandle = handle;
|
||||
} else if (
|
||||
pos.x >= sig.x &&
|
||||
pos.x <= sig.x + sig.width &&
|
||||
pos.y >= sig.y &&
|
||||
pos.y <= sig.y + sig.height
|
||||
) {
|
||||
foundSigId = sig.id;
|
||||
}
|
||||
|
||||
const pos = getMousePos(canvas, e);
|
||||
let foundSigId: any = null;
|
||||
let foundHandle = null;
|
||||
});
|
||||
|
||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
||||
if (foundSigId) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
foundSigId = sig.id;
|
||||
foundHandle = handle;
|
||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
||||
foundSigId = sig.id;
|
||||
}
|
||||
});
|
||||
canvas.className = '';
|
||||
if (foundHandle) {
|
||||
if (['top-left', 'bottom-right'].includes(foundHandle))
|
||||
canvas.classList.add('resize-nwse');
|
||||
else if (['top-right', 'bottom-left'].includes(foundHandle))
|
||||
canvas.classList.add('resize-nesw');
|
||||
else if (['top-middle', 'bottom-middle'].includes(foundHandle))
|
||||
canvas.classList.add('resize-ns');
|
||||
else if (['middle-left', 'middle-right'].includes(foundHandle))
|
||||
canvas.classList.add('resize-ew');
|
||||
} else if (foundSigId) {
|
||||
canvas.classList.add('movable');
|
||||
}
|
||||
|
||||
canvas.className = '';
|
||||
if (foundHandle) {
|
||||
if (['top-left', 'bottom-right'].includes(foundHandle)) canvas.classList.add('resize-nwse');
|
||||
else if (['top-right', 'bottom-left'].includes(foundHandle)) canvas.classList.add('resize-nesw');
|
||||
else if (['top-middle', 'bottom-middle'].includes(foundHandle)) canvas.classList.add('resize-ns');
|
||||
else if (['middle-left', 'middle-right'].includes(foundHandle)) canvas.classList.add('resize-ew');
|
||||
} else if (foundSigId) {
|
||||
canvas.classList.add('movable');
|
||||
if (signState.hoveredSigId !== foundSigId) {
|
||||
signState.hoveredSigId = foundSigId;
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||
document
|
||||
.getElementById('canvas-container-sign')
|
||||
.addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
||||
|
||||
const onDragStart = (e: any) => {
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
let clickedOnSignature = false;
|
||||
|
||||
signState.placedSignatures
|
||||
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||
.reverse()
|
||||
.forEach((sig) => {
|
||||
if (clickedOnSignature) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
signState.interactionMode = 'resize';
|
||||
signState.resizeHandle = handle;
|
||||
signState.draggedSigId = sig.id;
|
||||
clickedOnSignature = true;
|
||||
} else if (
|
||||
pos.x >= sig.x &&
|
||||
pos.x <= sig.x + sig.width &&
|
||||
pos.y >= sig.y &&
|
||||
pos.y <= sig.y + sig.height
|
||||
) {
|
||||
signState.interactionMode = 'drag';
|
||||
signState.draggedSigId = sig.id;
|
||||
signState.dragOffsetX = pos.x - sig.x;
|
||||
signState.dragOffsetY = pos.y - sig.y;
|
||||
clickedOnSignature = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (signState.hoveredSigId !== foundSigId) {
|
||||
signState.hoveredSigId = foundSigId;
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
if (clickedOnSignature) {
|
||||
ghost.classList.add('hidden');
|
||||
} else if (signState.activeSignature) {
|
||||
const sigWidth = 150;
|
||||
const sigHeight =
|
||||
(signState.activeSignature.image.height /
|
||||
signState.activeSignature.image.width) *
|
||||
sigWidth;
|
||||
signState.placedSignatures.push({
|
||||
id: Date.now(),
|
||||
image: signState.activeSignature.image,
|
||||
x: pos.x - sigWidth / 2,
|
||||
y: pos.y - sigHeight / 2,
|
||||
width: sigWidth,
|
||||
height: sigHeight,
|
||||
pageIndex: signState.currentPageNum - 1,
|
||||
aspectRatio: sigWidth / sigHeight,
|
||||
});
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
||||
const onDragMove = (e: any) => {
|
||||
if (signState.interactionMode === 'none') return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
const sig = signState.placedSignatures.find(
|
||||
(s) => s.id === signState.draggedSigId
|
||||
);
|
||||
if (!sig) return;
|
||||
|
||||
const onDragStart = (e: any) => {
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
let clickedOnSignature = false;
|
||||
|
||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
||||
if (clickedOnSignature) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
signState.interactionMode = 'resize';
|
||||
signState.resizeHandle = handle;
|
||||
signState.draggedSigId = sig.id;
|
||||
clickedOnSignature = true;
|
||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
||||
signState.interactionMode = 'drag';
|
||||
signState.draggedSigId = sig.id;
|
||||
signState.dragOffsetX = pos.x - sig.x;
|
||||
signState.dragOffsetY = pos.y - sig.y;
|
||||
clickedOnSignature = true;
|
||||
}
|
||||
});
|
||||
if (signState.interactionMode === 'drag') {
|
||||
sig.x = pos.x - signState.dragOffsetX;
|
||||
sig.y = pos.y - signState.dragOffsetY;
|
||||
} else if (signState.interactionMode === 'resize') {
|
||||
const originalRight = sig.x + sig.width;
|
||||
const originalBottom = sig.y + sig.height;
|
||||
|
||||
if (clickedOnSignature) {
|
||||
ghost.classList.add('hidden');
|
||||
} else if (signState.activeSignature) {
|
||||
const sigWidth = 150;
|
||||
const sigHeight = (signState.activeSignature.image.height / signState.activeSignature.image.width) * sigWidth;
|
||||
signState.placedSignatures.push({
|
||||
id: Date.now(), image: signState.activeSignature.image,
|
||||
x: pos.x - sigWidth / 2, y: pos.y - sigHeight / 2,
|
||||
width: sigWidth, height: sigHeight, pageIndex: signState.currentPageNum - 1,
|
||||
aspectRatio: sigWidth / sigHeight,
|
||||
});
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
|
||||
const onDragMove = (e: any) => {
|
||||
if (signState.interactionMode === 'none') return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
const sig = signState.placedSignatures.find(s => s.id === signState.draggedSigId);
|
||||
if (!sig) return;
|
||||
|
||||
if (signState.interactionMode === 'drag') {
|
||||
sig.x = pos.x - signState.dragOffsetX;
|
||||
sig.y = pos.y - signState.dragOffsetY;
|
||||
} else if (signState.interactionMode === 'resize') {
|
||||
const originalRight = sig.x + sig.width;
|
||||
const originalBottom = sig.y + sig.height;
|
||||
if (signState.resizeHandle.includes('right'))
|
||||
sig.width = Math.max(20, pos.x - sig.x);
|
||||
if (signState.resizeHandle.includes('bottom'))
|
||||
sig.height = Math.max(20, pos.y - sig.y);
|
||||
if (signState.resizeHandle.includes('left')) {
|
||||
sig.width = Math.max(20, originalRight - pos.x);
|
||||
sig.x = originalRight - sig.width;
|
||||
}
|
||||
if (signState.resizeHandle.includes('top')) {
|
||||
sig.height = Math.max(20, originalBottom - pos.y);
|
||||
sig.y = originalBottom - sig.height;
|
||||
}
|
||||
|
||||
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
|
||||
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
|
||||
if (signState.resizeHandle.includes('left')) {
|
||||
sig.width = Math.max(20, originalRight - pos.x);
|
||||
sig.x = originalRight - sig.width;
|
||||
}
|
||||
if (signState.resizeHandle.includes('top')) {
|
||||
sig.height = Math.max(20, originalBottom - pos.y);
|
||||
sig.y = originalBottom - sig.height;
|
||||
}
|
||||
if (
|
||||
signState.resizeHandle.includes('left') ||
|
||||
signState.resizeHandle.includes('right')
|
||||
) {
|
||||
sig.height = sig.width / sig.aspectRatio;
|
||||
} else if (
|
||||
signState.resizeHandle.includes('top') ||
|
||||
signState.resizeHandle.includes('bottom')
|
||||
) {
|
||||
sig.width = sig.height * sig.aspectRatio;
|
||||
}
|
||||
}
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
if (signState.resizeHandle.includes('left') || signState.resizeHandle.includes('right')) {
|
||||
sig.height = sig.width / sig.aspectRatio;
|
||||
} else if (signState.resizeHandle.includes('top') || signState.resizeHandle.includes('bottom')) {
|
||||
sig.width = sig.height * sig.aspectRatio;
|
||||
}
|
||||
}
|
||||
drawSignatures();
|
||||
};
|
||||
const onDragEnd = () => {
|
||||
signState.interactionMode = 'none';
|
||||
signState.draggedSigId = null;
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
signState.interactionMode = 'none';
|
||||
signState.draggedSigId = null;
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
['mousedown', 'touchstart'].forEach(evt => canvas.addEventListener(evt, onDragStart, { passive: false }));
|
||||
['mousemove', 'touchmove'].forEach(evt => canvas.addEventListener(evt, onDragMove, { passive: false }));
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => canvas.addEventListener(evt, onDragEnd));
|
||||
['mousedown', 'touchstart'].forEach((evt) =>
|
||||
canvas.addEventListener(evt, onDragStart, { passive: false })
|
||||
);
|
||||
['mousemove', 'touchmove'].forEach((evt) =>
|
||||
canvas.addEventListener(evt, onDragMove, { passive: false })
|
||||
);
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
|
||||
canvas.addEventListener(evt, onDragEnd)
|
||||
);
|
||||
}
|
||||
|
||||
export async function setupSignTool() {
|
||||
document.getElementById('signature-editor').classList.remove('hidden');
|
||||
|
||||
signState.canvas = document.getElementById('canvas-sign');
|
||||
signState.context = signState.canvas.getContext('2d');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
document.getElementById('total-pages-display-sign').textContent = signState.pdf.numPages;
|
||||
|
||||
await fitToWidth();
|
||||
setupDrawingCanvas();
|
||||
setupPlacementListeners();
|
||||
document.getElementById('signature-editor').classList.remove('hidden');
|
||||
|
||||
document.getElementById('prev-page-sign').onclick = () => { if (signState.currentPageNum > 1) { signState.currentPageNum--; renderPage(signState.currentPageNum); } };
|
||||
document.getElementById('next-page-sign').onclick = () => { if (signState.currentPageNum < signState.pdf.numPages) { signState.currentPageNum++; renderPage(signState.currentPageNum); } };
|
||||
document.getElementById('zoom-in-btn').onclick = () => { signState.scale += 0.25; renderPage(signState.currentPageNum); };
|
||||
document.getElementById('zoom-out-btn').onclick = () => { signState.scale = Math.max(0.25, signState.scale - 0.25); renderPage(signState.currentPageNum); };
|
||||
document.getElementById('fit-width-btn').onclick = fitToWidth;
|
||||
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
|
||||
|
||||
const tabs = ['draw', 'type', 'upload'];
|
||||
const tabButtons = tabs.map(t => document.getElementById(`${t}-tab-btn`));
|
||||
const tabPanels = tabs.map(t => document.getElementById(`${t}-panel`));
|
||||
tabButtons.forEach((button, index) => {
|
||||
button.onclick = () => {
|
||||
tabPanels.forEach(panel => panel.classList.add('hidden'));
|
||||
tabButtons.forEach(btn => {
|
||||
btn.classList.remove('border-indigo-500', 'text-white');
|
||||
btn.classList.add('border-transparent', 'text-gray-400');
|
||||
});
|
||||
tabPanels[index].classList.remove('hidden');
|
||||
button.classList.add('border-indigo-500', 'text-white');
|
||||
button.classList.remove('border-transparent', 'text-gray-400');
|
||||
};
|
||||
signState.canvas = document.getElementById('canvas-sign');
|
||||
signState.context = signState.canvas.getContext('2d');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
document.getElementById('total-pages-display-sign').textContent =
|
||||
signState.pdf.numPages;
|
||||
|
||||
await fitToWidth();
|
||||
setupDrawingCanvas();
|
||||
setupPlacementListeners();
|
||||
|
||||
document.getElementById('prev-page-sign').onclick = () => {
|
||||
if (signState.currentPageNum > 1) {
|
||||
signState.currentPageNum--;
|
||||
renderPage(signState.currentPageNum);
|
||||
}
|
||||
};
|
||||
document.getElementById('next-page-sign').onclick = () => {
|
||||
if (signState.currentPageNum < signState.pdf.numPages) {
|
||||
signState.currentPageNum++;
|
||||
renderPage(signState.currentPageNum);
|
||||
}
|
||||
};
|
||||
document.getElementById('zoom-in-btn').onclick = () => {
|
||||
signState.scale += 0.25;
|
||||
renderPage(signState.currentPageNum);
|
||||
};
|
||||
document.getElementById('zoom-out-btn').onclick = () => {
|
||||
signState.scale = Math.max(0.25, signState.scale - 0.25);
|
||||
renderPage(signState.currentPageNum);
|
||||
};
|
||||
document.getElementById('fit-width-btn').onclick = fitToWidth;
|
||||
document.getElementById('undo-btn').onclick = () => {
|
||||
signState.placedSignatures.pop();
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
const tabs = ['draw', 'type', 'upload'];
|
||||
const tabButtons = tabs.map((t) => document.getElementById(`${t}-tab-btn`));
|
||||
const tabPanels = tabs.map((t) => document.getElementById(`${t}-panel`));
|
||||
tabButtons.forEach((button, index) => {
|
||||
button.onclick = () => {
|
||||
tabPanels.forEach((panel) => panel.classList.add('hidden'));
|
||||
tabButtons.forEach((btn) => {
|
||||
btn.classList.remove('border-indigo-500', 'text-white');
|
||||
btn.classList.add('border-transparent', 'text-gray-400');
|
||||
});
|
||||
tabPanels[index].classList.remove('hidden');
|
||||
button.classList.add('border-indigo-500', 'text-white');
|
||||
button.classList.remove('border-transparent', 'text-gray-400');
|
||||
};
|
||||
});
|
||||
|
||||
document.getElementById('clear-draw-btn').onclick = () =>
|
||||
signState.drawContext.clearRect(
|
||||
0,
|
||||
0,
|
||||
signState.drawCanvas.width,
|
||||
signState.drawCanvas.height
|
||||
);
|
||||
document.getElementById('save-draw-btn').onclick = () => {
|
||||
addSignatureToSaved(signState.drawCanvas.toDataURL());
|
||||
signState.drawContext.clearRect(
|
||||
0,
|
||||
0,
|
||||
signState.drawCanvas.width,
|
||||
signState.drawCanvas.height
|
||||
);
|
||||
};
|
||||
|
||||
const textInput = document.getElementById('signature-text-input');
|
||||
const fontPreview = document.getElementById('font-preview');
|
||||
const fontFamilySelect = document.getElementById('font-family-select');
|
||||
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||
const fontSizeValue = document.getElementById('font-size-value');
|
||||
const fontColorPicker = document.getElementById('font-color-picker');
|
||||
|
||||
const updateFontPreview = () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.textContent = textInput.value || 'Your Name';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontFamily = fontFamilySelect.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.color = fontColorPicker.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontSizeValue.textContent = fontSizeSlider.value;
|
||||
};
|
||||
|
||||
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach((el) =>
|
||||
el.addEventListener('input', updateFontPreview)
|
||||
);
|
||||
updateFontPreview();
|
||||
|
||||
document.getElementById('save-type-btn').onclick = async () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
if (!textInput.value) return;
|
||||
const canvas = await html2canvas(fontPreview, {
|
||||
backgroundColor: null,
|
||||
scale: 2,
|
||||
});
|
||||
addSignatureToSaved(canvas.toDataURL());
|
||||
};
|
||||
|
||||
document.getElementById('clear-draw-btn').onclick = () => signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height);
|
||||
document.getElementById('save-draw-btn').onclick = () => { addSignatureToSaved(signState.drawCanvas.toDataURL()); signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height); };
|
||||
|
||||
const textInput = document.getElementById('signature-text-input');
|
||||
const fontPreview = document.getElementById('font-preview');
|
||||
const fontFamilySelect = document.getElementById('font-family-select');
|
||||
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||
const fontSizeValue = document.getElementById('font-size-value');
|
||||
const fontColorPicker = document.getElementById('font-color-picker');
|
||||
|
||||
const updateFontPreview = () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.textContent = textInput.value || 'Your Name';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontFamily = fontFamilySelect.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.color = fontColorPicker.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontSizeValue.textContent = fontSizeSlider.value;
|
||||
};
|
||||
|
||||
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach(el => el.addEventListener('input', updateFontPreview));
|
||||
updateFontPreview();
|
||||
|
||||
document.getElementById('save-type-btn').onclick = async () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
if (!textInput.value) return;
|
||||
const canvas = await html2canvas(fontPreview, { backgroundColor: null, scale: 2 });
|
||||
addSignatureToSaved(canvas.toDataURL());
|
||||
};
|
||||
|
||||
document.getElementById('signature-upload-input').onchange = (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => addSignatureToSaved(event.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
document.getElementById('signature-upload-input').onchange = (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => addSignatureToSaved(event.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyAndSaveSignatures() {
|
||||
if (signState.placedSignatures.length === 0) {
|
||||
showAlert('No Signatures Placed', 'Please place at least one signature.');
|
||||
return;
|
||||
}
|
||||
showLoader('Applying signatures...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
for (const sig of signState.placedSignatures) {
|
||||
const page = pages[sig.pageIndex];
|
||||
const originalPageSize = page.getSize();
|
||||
const pngBytes = await fetch(sig.image.src).then(res => res.arrayBuffer());
|
||||
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
||||
|
||||
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
||||
const renderedViewport = renderedPage.getViewport({ scale: signState.scale });
|
||||
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
||||
if (signState.placedSignatures.length === 0) {
|
||||
showAlert('No Signatures Placed', 'Please place at least one signature.');
|
||||
return;
|
||||
}
|
||||
showLoader('Applying signatures...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
for (const sig of signState.placedSignatures) {
|
||||
const page = pages[sig.pageIndex];
|
||||
const originalPageSize = page.getSize();
|
||||
const pngBytes = await fetch(sig.image.src).then((res) =>
|
||||
res.arrayBuffer()
|
||||
);
|
||||
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
||||
|
||||
page.drawImage(pngImage, {
|
||||
x: sig.x * scaleRatio,
|
||||
y: originalPageSize.height - (sig.y * scaleRatio) - (sig.height * scaleRatio),
|
||||
width: sig.width * scaleRatio,
|
||||
height: sig.height * scaleRatio,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'signed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply signatures.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
||||
const renderedViewport = renderedPage.getViewport({
|
||||
scale: signState.scale,
|
||||
});
|
||||
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
||||
|
||||
page.drawImage(pngImage, {
|
||||
x: sig.x * scaleRatio,
|
||||
y:
|
||||
originalPageSize.height -
|
||||
sig.y * scaleRatio -
|
||||
sig.height * scaleRatio,
|
||||
width: sig.width * scaleRatio,
|
||||
height: sig.height * scaleRatio,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'signed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply signatures.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,49 +5,51 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function splitInHalf() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'No PDF document is loaded.');
|
||||
return;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'No PDF document is loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Splitting PDF pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
const whiteColor = rgb(1, 1, 1); // For masking
|
||||
|
||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||
|
||||
// Copy the page twice for all split types
|
||||
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
|
||||
switch (splitType) {
|
||||
case 'vertical':
|
||||
page1.setCropBox(0, 0, width / 2, height);
|
||||
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||
break;
|
||||
case 'horizontal':
|
||||
page1.setCropBox(0, height / 2, width, height / 2); // Top half
|
||||
page2.setCropBox(0, 0, width, height / 2); // Bottom half
|
||||
break;
|
||||
}
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
}
|
||||
showLoader('Splitting PDF pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
const whiteColor = rgb(1, 1, 1); // For masking
|
||||
|
||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||
|
||||
// Copy the page twice for all split types
|
||||
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
|
||||
switch (splitType) {
|
||||
case 'vertical':
|
||||
page1.setCropBox(0, 0, width / 2, height);
|
||||
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||
break;
|
||||
case 'horizontal':
|
||||
page1.setCropBox(0, height / 2, width, height / 2); // Top half
|
||||
page2.setCropBox(0, 0, width, height / 2); // Bottom half
|
||||
break;
|
||||
}
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'split-half.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while splitting the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'split-half.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while splitting the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
@@ -9,205 +8,232 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
// Track if visual selector has been rendered to avoid duplicates
|
||||
let visualSelectorRendered = false;
|
||||
|
||||
|
||||
async function renderVisualSelector() {
|
||||
if (visualSelectorRendered) return;
|
||||
if (visualSelectorRendered) return;
|
||||
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (!container) return;
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (!container) return;
|
||||
|
||||
visualSelectorRendered = true;
|
||||
visualSelectorRendered = true;
|
||||
|
||||
container.textContent = '';
|
||||
|
||||
showLoader('Rendering page previews...');
|
||||
try {
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
container.textContent = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.4 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.pageIndex = i - 1;
|
||||
showLoader('Rendering page previews...');
|
||||
try {
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'rounded-md w-full h-auto';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-center text-xs mt-1 text-gray-300';
|
||||
p.textContent = `Page ${i}`;
|
||||
wrapper.append(img, p);
|
||||
|
||||
const handleSelection = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isSelected = wrapper.classList.contains('selected');
|
||||
|
||||
if (isSelected) {
|
||||
wrapper.classList.remove('selected', 'border-indigo-500');
|
||||
wrapper.classList.add('border-transparent');
|
||||
} else {
|
||||
wrapper.classList.add('selected', 'border-indigo-500');
|
||||
wrapper.classList.remove('border-transparent');
|
||||
}
|
||||
};
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.4 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: viewport,
|
||||
}).promise;
|
||||
|
||||
wrapper.addEventListener('click', handleSelection);
|
||||
wrapper.addEventListener('touchend', handleSelection);
|
||||
|
||||
wrapper.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
container.appendChild(wrapper);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.pageIndex = i - 1;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'rounded-md w-full h-auto';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-center text-xs mt-1 text-gray-300';
|
||||
p.textContent = `Page ${i}`;
|
||||
wrapper.append(img, p);
|
||||
|
||||
const handleSelection = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isSelected = wrapper.classList.contains('selected');
|
||||
|
||||
if (isSelected) {
|
||||
wrapper.classList.remove('selected', 'border-indigo-500');
|
||||
wrapper.classList.add('border-transparent');
|
||||
} else {
|
||||
wrapper.classList.add('selected', 'border-indigo-500');
|
||||
wrapper.classList.remove('border-transparent');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rendering visual selector:', error);
|
||||
showAlert('Error', 'Failed to render page previews.');
|
||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
||||
visualSelectorRendered = false;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addEventListener('click', handleSelection);
|
||||
wrapper.addEventListener('touchend', handleSelection);
|
||||
|
||||
wrapper.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rendering visual selector:', error);
|
||||
showAlert('Error', 'Failed to render page previews.');
|
||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
||||
visualSelectorRendered = false;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export function setupSplitTool() {
|
||||
const splitModeSelect = document.getElementById('split-mode');
|
||||
const rangePanel = document.getElementById('range-panel');
|
||||
const visualPanel = document.getElementById('visual-select-panel');
|
||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
||||
const allPagesPanel = document.getElementById('all-pages-panel');
|
||||
const splitModeSelect = document.getElementById('split-mode');
|
||||
const rangePanel = document.getElementById('range-panel');
|
||||
const visualPanel = document.getElementById('visual-select-panel');
|
||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
||||
const allPagesPanel = document.getElementById('all-pages-panel');
|
||||
|
||||
if (!splitModeSelect) return;
|
||||
if (!splitModeSelect) return;
|
||||
|
||||
splitModeSelect.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
const mode = e.target.value;
|
||||
|
||||
if (mode !== 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
|
||||
rangePanel.classList.add('hidden');
|
||||
visualPanel.classList.add('hidden');
|
||||
evenOddPanel.classList.add('hidden');
|
||||
allPagesPanel.classList.add('hidden');
|
||||
zipOptionWrapper.classList.add('hidden');
|
||||
|
||||
if (mode === 'range') {
|
||||
rangePanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
} else if (mode === 'visual') {
|
||||
visualPanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
renderVisualSelector();
|
||||
} else if (mode === 'even-odd') {
|
||||
evenOddPanel.classList.remove('hidden');
|
||||
} else if (mode === 'all') {
|
||||
allPagesPanel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
splitModeSelect.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
const mode = e.target.value;
|
||||
|
||||
if (mode !== 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
|
||||
rangePanel.classList.add('hidden');
|
||||
visualPanel.classList.add('hidden');
|
||||
evenOddPanel.classList.add('hidden');
|
||||
allPagesPanel.classList.add('hidden');
|
||||
zipOptionWrapper.classList.add('hidden');
|
||||
|
||||
if (mode === 'range') {
|
||||
rangePanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
} else if (mode === 'visual') {
|
||||
visualPanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
renderVisualSelector();
|
||||
} else if (mode === 'even-odd') {
|
||||
evenOddPanel.classList.remove('hidden');
|
||||
} else if (mode === 'all') {
|
||||
allPagesPanel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function split() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitMode = document.getElementById('split-mode').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const downloadAsZip = document.getElementById('download-as-zip')?.checked || false;
|
||||
|
||||
showLoader('Splitting PDF...');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitMode = document.getElementById('split-mode').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const downloadAsZip =
|
||||
document.getElementById('download-as-zip')?.checked || false;
|
||||
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let indicesToExtract: any = [];
|
||||
|
||||
switch (splitMode) {
|
||||
case 'range':
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
if (!pageRangeInput) throw new Error('Please enter a page range.');
|
||||
const ranges = pageRangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'even-odd':
|
||||
const choiceElement = document.querySelector('input[name="even-odd-choice"]:checked');
|
||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const choice = choiceElement.value;
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||
break;
|
||||
case 'visual':
|
||||
indicesToExtract = Array.from(document.querySelectorAll('.page-thumbnail-wrapper.selected'))
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map(el => parseInt(el.dataset.pageIndex));
|
||||
break;
|
||||
showLoader('Splitting PDF...');
|
||||
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let indicesToExtract: any = [];
|
||||
|
||||
switch (splitMode) {
|
||||
case 'range':
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
if (!pageRangeInput) throw new Error('Please enter a page range.');
|
||||
const ranges = pageRangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||
if (uniqueIndices.length === 0) {
|
||||
throw new Error('No pages were selected for splitting.');
|
||||
break;
|
||||
|
||||
case 'even-odd':
|
||||
const choiceElement = document.querySelector(
|
||||
'input[name="even-odd-choice"]:checked'
|
||||
);
|
||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const choice = choiceElement.value;
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||
}
|
||||
|
||||
if (splitMode === 'all' || (['range', 'visual'].includes(splitMode) && downloadAsZip)) {
|
||||
showLoader('Creating ZIP file...');
|
||||
const zip = new JSZip();
|
||||
for (const index of uniqueIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const pdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'split-pages.zip');
|
||||
} else {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, uniqueIndices as number[]);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
const pdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'split-document.pdf');
|
||||
}
|
||||
|
||||
if (splitMode === 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to split PDF. Please check your selection.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
break;
|
||||
case 'all':
|
||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||
break;
|
||||
case 'visual':
|
||||
indicesToExtract = Array.from(
|
||||
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||
)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map((el) => parseInt(el.dataset.pageIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||
if (uniqueIndices.length === 0) {
|
||||
throw new Error('No pages were selected for splitting.');
|
||||
}
|
||||
|
||||
if (
|
||||
splitMode === 'all' ||
|
||||
(['range', 'visual'].includes(splitMode) && downloadAsZip)
|
||||
) {
|
||||
showLoader('Creating ZIP file...');
|
||||
const zip = new JSZip();
|
||||
for (const index of uniqueIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||
index as number,
|
||||
]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const pdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'split-pages.zip');
|
||||
} else {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(
|
||||
state.pdfDoc,
|
||||
uniqueIndices as number[]
|
||||
);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
const pdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'split-document.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
if (splitMode === 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
e.message || 'Failed to split PDF. Please check your selection.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,51 +5,64 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
async function convertImageToPngBytes(file: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function svgToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one SVG file.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one SVG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting SVG to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
showLoader('Converting SVG to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_svgs.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_svgs.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert SVG to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,78 +5,91 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { decode } from 'tiff';
|
||||
|
||||
export async function tiffToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one TIFF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting TIFF to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const tiffBytes = await readFileAsArrayBuffer(file);
|
||||
const ifds = decode(tiffBytes as any);
|
||||
|
||||
for (const ifd of ifds) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = ifd.width;
|
||||
canvas.height = ifd.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get canvas context');
|
||||
}
|
||||
|
||||
const imageData = ctx.createImageData(ifd.width, ifd.height);
|
||||
const pixels = imageData.data;
|
||||
|
||||
// Calculate samples per pixel from data length
|
||||
const totalPixels = ifd.width * ifd.height;
|
||||
const samplesPerPixel = ifd.data.length / totalPixels;
|
||||
|
||||
// Convert TIFF data to RGBA
|
||||
for (let i = 0; i < totalPixels; i++) {
|
||||
const dstIndex = i * 4;
|
||||
|
||||
if (samplesPerPixel === 1) {
|
||||
// Grayscale
|
||||
const gray = ifd.data[i];
|
||||
pixels[dstIndex] = gray;
|
||||
pixels[dstIndex + 1] = gray;
|
||||
pixels[dstIndex + 2] = gray;
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 3) {
|
||||
// RGB
|
||||
const srcIndex = i * 3;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 4) {
|
||||
// RGBA
|
||||
const srcIndex = i * 4;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngBlob = await new Promise<Blob>((res) => canvas.toBlob(res!, 'image/png'));
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one TIFF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting TIFF to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const tiffBytes = await readFileAsArrayBuffer(file);
|
||||
const ifds = decode(tiffBytes as any);
|
||||
|
||||
for (const ifd of ifds) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = ifd.width;
|
||||
canvas.height = ifd.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get canvas context');
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_tiff.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
|
||||
const imageData = ctx.createImageData(ifd.width, ifd.height);
|
||||
const pixels = imageData.data;
|
||||
|
||||
// Calculate samples per pixel from data length
|
||||
const totalPixels = ifd.width * ifd.height;
|
||||
const samplesPerPixel = ifd.data.length / totalPixels;
|
||||
|
||||
// Convert TIFF data to RGBA
|
||||
for (let i = 0; i < totalPixels; i++) {
|
||||
const dstIndex = i * 4;
|
||||
|
||||
if (samplesPerPixel === 1) {
|
||||
// Grayscale
|
||||
const gray = ifd.data[i];
|
||||
pixels[dstIndex] = gray;
|
||||
pixels[dstIndex + 1] = gray;
|
||||
pixels[dstIndex + 2] = gray;
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 3) {
|
||||
// RGB
|
||||
const srcIndex = i * 3;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 4) {
|
||||
// RGBA
|
||||
const srcIndex = i * 4;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngBlob = await new Promise<Blob>((res) =>
|
||||
canvas.toBlob(res!, 'image/png')
|
||||
);
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_tiff.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,95 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
rgb,
|
||||
StandardFonts,
|
||||
PageSizes,
|
||||
} from 'pdf-lib';
|
||||
|
||||
export async function txtToPdf() {
|
||||
showLoader('Creating PDF...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('text-input').value;
|
||||
if (!text.trim()) {
|
||||
showAlert('Input Required', 'Please enter some text to convert.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontFamilyKey = document.getElementById('font-family').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
|
||||
const pageSize = PageSizes[pageSizeKey];
|
||||
const margin = 72; // 1 inch
|
||||
|
||||
let page = pdfDoc.addPage(pageSize);
|
||||
let { width, height } = page.getSize();
|
||||
const textWidth = width - margin * 2;
|
||||
const lineHeight = fontSize * 1.3;
|
||||
let y = height - margin;
|
||||
|
||||
const paragraphs = text.split('\n');
|
||||
for (const paragraph of paragraphs) {
|
||||
const words = paragraph.split(' ');
|
||||
let currentLine = '';
|
||||
for (const word of words) {
|
||||
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
||||
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
||||
currentLine = testLine;
|
||||
} else {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
||||
y -= lineHeight;
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
if (currentLine.length > 0) {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
||||
y -= lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'text-document.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create PDF from text.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
showLoader('Creating PDF...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('text-input').value;
|
||||
if (!text.trim()) {
|
||||
showAlert('Input Required', 'Please enter some text to convert.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontFamilyKey = document.getElementById('font-family').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
|
||||
const pageSize = PageSizes[pageSizeKey];
|
||||
const margin = 72; // 1 inch
|
||||
|
||||
let page = pdfDoc.addPage(pageSize);
|
||||
let { width, height } = page.getSize();
|
||||
const textWidth = width - margin * 2;
|
||||
const lineHeight = fontSize * 1.3;
|
||||
let y = height - margin;
|
||||
|
||||
const paragraphs = text.split('\n');
|
||||
for (const paragraph of paragraphs) {
|
||||
const words = paragraph.split(' ');
|
||||
let currentLine = '';
|
||||
for (const word of words) {
|
||||
const testLine =
|
||||
currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
||||
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
||||
currentLine = testLine;
|
||||
} else {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, {
|
||||
x: margin,
|
||||
y,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
});
|
||||
y -= lineHeight;
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
if (currentLine.length > 0) {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, {
|
||||
x: margin,
|
||||
y,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
});
|
||||
y -= lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'text-document.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create PDF from text.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
|
||||
export function viewMetadata() {
|
||||
console.log("");
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
@@ -5,44 +5,52 @@ import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function webpToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one WebP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting WebP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const webpBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
|
||||
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one WebP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting WebP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const webpBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
|
||||
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
|
||||
|
||||
const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
// Embed the converted PNG into the PDF
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_webp.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
const pngBlob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
// Embed the converted PNG into the PDF
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_webp.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert WebP to PDF. Ensure all files are valid WebP images.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,39 @@ import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function wordToPdf() {
|
||||
const file = state.files[0];
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a .docx file first.');
|
||||
return;
|
||||
}
|
||||
const file = state.files[0];
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a .docx file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing preview...');
|
||||
|
||||
try {
|
||||
showLoader('Preparing preview...');
|
||||
|
||||
const mammothOptions = {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
convertImage: mammoth.images.inline((element: any) => {
|
||||
return element.read("base64").then((imageBuffer: any) => {
|
||||
return {
|
||||
src: `data:${element.contentType};base64,${imageBuffer}`
|
||||
};
|
||||
});
|
||||
})
|
||||
};
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
const { value: html } = await mammoth.convertToHtml({ arrayBuffer }, mammothOptions);
|
||||
|
||||
// Get references to our modal elements from index.html
|
||||
const previewModal = document.getElementById('preview-modal');
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
const downloadBtn = document.getElementById('preview-download-btn');
|
||||
const closeBtn = document.getElementById('preview-close-btn');
|
||||
try {
|
||||
const mammothOptions = {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
convertImage: mammoth.images.inline((element: any) => {
|
||||
return element.read('base64').then((imageBuffer: any) => {
|
||||
return {
|
||||
src: `data:${element.contentType};base64,${imageBuffer}`,
|
||||
};
|
||||
});
|
||||
}),
|
||||
};
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
const { value: html } = await mammoth.convertToHtml(
|
||||
{ arrayBuffer },
|
||||
mammothOptions
|
||||
);
|
||||
|
||||
const styledHtml = `
|
||||
// Get references to our modal elements from index.html
|
||||
const previewModal = document.getElementById('preview-modal');
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
const downloadBtn = document.getElementById('preview-download-btn');
|
||||
const closeBtn = document.getElementById('preview-close-btn');
|
||||
|
||||
const styledHtml = `
|
||||
<style>
|
||||
#preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
|
||||
#preview-content table { border-collapse: collapse; width: 100%; }
|
||||
@@ -44,88 +46,95 @@ export async function wordToPdf() {
|
||||
</style>
|
||||
${html}
|
||||
`;
|
||||
previewContent.innerHTML = styledHtml;
|
||||
|
||||
const marginDiv = document.createElement('div');
|
||||
marginDiv.style.height = '100px';
|
||||
previewContent.appendChild(marginDiv);
|
||||
previewContent.innerHTML = styledHtml;
|
||||
|
||||
const images = previewContent.querySelectorAll('img');
|
||||
const imagePromises = Array.from(images).map(img => {
|
||||
return new Promise((resolve) => {
|
||||
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
|
||||
if (img.complete) resolve();
|
||||
else img.onload = resolve;
|
||||
});
|
||||
});
|
||||
await Promise.all(imagePromises);
|
||||
|
||||
const marginDiv = document.createElement('div');
|
||||
marginDiv.style.height = '100px';
|
||||
previewContent.appendChild(marginDiv);
|
||||
|
||||
previewModal.classList.remove('hidden');
|
||||
hideLoader();
|
||||
const images = previewContent.querySelectorAll('img');
|
||||
const imagePromises = Array.from(images).map((img) => {
|
||||
return new Promise((resolve) => {
|
||||
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
|
||||
if (img.complete) resolve();
|
||||
else img.onload = resolve;
|
||||
});
|
||||
});
|
||||
await Promise.all(imagePromises);
|
||||
|
||||
const downloadHandler = async () => {
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF({
|
||||
orientation: 'p',
|
||||
unit: 'pt',
|
||||
format: 'letter'
|
||||
});
|
||||
previewModal.classList.remove('hidden');
|
||||
hideLoader();
|
||||
|
||||
await doc.html(previewContent, {
|
||||
callback: function (doc: any) {
|
||||
const links = previewContent.querySelectorAll('a');
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||
const downloadHandler = async () => {
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link.href) return;
|
||||
|
||||
const linkRect = link.getBoundingClientRect();
|
||||
|
||||
// Calculate position relative to the preview container's top-left
|
||||
const relativeX = linkRect.left - containerRect.left;
|
||||
const relativeY = linkRect.top - containerRect.top;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF({
|
||||
orientation: 'p',
|
||||
unit: 'pt',
|
||||
format: 'letter',
|
||||
});
|
||||
|
||||
const pageNum = Math.floor(relativeY / pageHeight) + 1;
|
||||
const yOnPage = relativeY % pageHeight;
|
||||
await doc.html(previewContent, {
|
||||
callback: function (doc: any) {
|
||||
const links = previewContent.querySelectorAll('a');
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||
|
||||
doc.setPage(pageNum);
|
||||
try {
|
||||
doc.link(relativeX + 45, yOnPage + 45, linkRect.width, linkRect.height, { url: link.href });
|
||||
} catch (e) {
|
||||
console.warn("Could not add link:", link.href, e);
|
||||
}
|
||||
});
|
||||
links.forEach((link) => {
|
||||
if (!link.href) return;
|
||||
|
||||
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
|
||||
doc.save(outputFileName);
|
||||
hideLoader();
|
||||
},
|
||||
autoPaging: 'slice',
|
||||
x: 45,
|
||||
y: 45,
|
||||
width: 522,
|
||||
windowWidth: previewContent.scrollWidth
|
||||
});
|
||||
};
|
||||
const linkRect = link.getBoundingClientRect();
|
||||
|
||||
const closeHandler = () => {
|
||||
previewModal.classList.add('hidden');
|
||||
previewContent.innerHTML = '';
|
||||
downloadBtn.removeEventListener('click', downloadHandler);
|
||||
closeBtn.removeEventListener('click', closeHandler);
|
||||
};
|
||||
// Calculate position relative to the preview container's top-left
|
||||
const relativeX = linkRect.left - containerRect.left;
|
||||
const relativeY = linkRect.top - containerRect.top;
|
||||
|
||||
downloadBtn.addEventListener('click', downloadHandler);
|
||||
closeBtn.addEventListener('click', closeHandler);
|
||||
const pageNum = Math.floor(relativeY / pageHeight) + 1;
|
||||
const yOnPage = relativeY % pageHeight;
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert('Preview Error', `Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
doc.setPage(pageNum);
|
||||
try {
|
||||
doc.link(
|
||||
relativeX + 45,
|
||||
yOnPage + 45,
|
||||
linkRect.width,
|
||||
linkRect.height,
|
||||
{ url: link.href }
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Could not add link:', link.href, e);
|
||||
}
|
||||
});
|
||||
|
||||
const outputFileName = `${file.name.replace(/\.[^/.]+$/, '')}.pdf`;
|
||||
doc.save(outputFileName);
|
||||
hideLoader();
|
||||
},
|
||||
autoPaging: 'slice',
|
||||
x: 45,
|
||||
y: 45,
|
||||
width: 522,
|
||||
windowWidth: previewContent.scrollWidth,
|
||||
});
|
||||
};
|
||||
|
||||
const closeHandler = () => {
|
||||
previewModal.classList.add('hidden');
|
||||
previewContent.innerHTML = '';
|
||||
downloadBtn.removeEventListener('click', downloadHandler);
|
||||
closeBtn.removeEventListener('click', closeHandler);
|
||||
};
|
||||
|
||||
downloadBtn.addEventListener('click', downloadHandler);
|
||||
closeBtn.addEventListener('click', closeHandler);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Preview Error',
|
||||
`Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
176
src/js/main.ts
176
src/js/main.ts
@@ -3,115 +3,119 @@ import { dom, switchView, hideAlert } from './ui.js';
|
||||
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import "../css/styles.css";
|
||||
import '../css/styles.css';
|
||||
|
||||
const init = () => {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
dom.toolGrid.textContent = '';
|
||||
dom.toolGrid.textContent = '';
|
||||
|
||||
categories.forEach(category => {
|
||||
const categoryGroup = document.createElement('div');
|
||||
categoryGroup.className = 'category-group col-span-full';
|
||||
categories.forEach((category) => {
|
||||
const categoryGroup = document.createElement('div');
|
||||
categoryGroup.className = 'category-group col-span-full';
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
|
||||
title.textContent = category.name;
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
|
||||
title.textContent = category.name;
|
||||
|
||||
const toolsContainer = document.createElement('div');
|
||||
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
||||
const toolsContainer = document.createElement('div');
|
||||
toolsContainer.className =
|
||||
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
||||
|
||||
category.tools.forEach(tool => {
|
||||
const toolCard = document.createElement('div');
|
||||
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
||||
toolCard.dataset.toolId = tool.id;
|
||||
category.tools.forEach((tool) => {
|
||||
const toolCard = document.createElement('div');
|
||||
toolCard.className =
|
||||
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
||||
toolCard.dataset.toolId = tool.id;
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
||||
icon.setAttribute('data-lucide', tool.icon);
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
||||
icon.setAttribute('data-lucide', tool.icon);
|
||||
|
||||
const toolName = document.createElement('h3');
|
||||
toolName.className = 'font-semibold text-white';
|
||||
toolName.textContent = tool.name;
|
||||
const toolName = document.createElement('h3');
|
||||
toolName.className = 'font-semibold text-white';
|
||||
toolName.textContent = tool.name;
|
||||
|
||||
toolCard.append(icon, toolName);
|
||||
toolCard.append(icon, toolName);
|
||||
|
||||
if (tool.subtitle) {
|
||||
const toolSubtitle = document.createElement('p');
|
||||
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
|
||||
toolSubtitle.textContent = tool.subtitle;
|
||||
toolCard.appendChild(toolSubtitle);
|
||||
}
|
||||
if (tool.subtitle) {
|
||||
const toolSubtitle = document.createElement('p');
|
||||
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
|
||||
toolSubtitle.textContent = tool.subtitle;
|
||||
toolCard.appendChild(toolSubtitle);
|
||||
}
|
||||
|
||||
toolsContainer.appendChild(toolCard);
|
||||
});
|
||||
|
||||
categoryGroup.append(title, toolsContainer);
|
||||
dom.toolGrid.appendChild(categoryGroup);
|
||||
toolsContainer.appendChild(toolCard);
|
||||
});
|
||||
|
||||
const searchBar = document.getElementById('search-bar');
|
||||
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
||||
categoryGroup.append(title, toolsContainer);
|
||||
dom.toolGrid.appendChild(categoryGroup);
|
||||
});
|
||||
|
||||
searchBar.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||
const searchBar = document.getElementById('search-bar');
|
||||
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
||||
|
||||
categoryGroups.forEach(group => {
|
||||
const toolCards = group.querySelectorAll('.tool-card');
|
||||
let visibleToolsInCategory = 0;
|
||||
searchBar.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||
|
||||
toolCards.forEach(card => {
|
||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
||||
categoryGroups.forEach((group) => {
|
||||
const toolCards = group.querySelectorAll('.tool-card');
|
||||
let visibleToolsInCategory = 0;
|
||||
|
||||
card.classList.toggle('hidden', !isMatch);
|
||||
if (isMatch) {
|
||||
visibleToolsInCategory++;
|
||||
}
|
||||
});
|
||||
toolCards.forEach((card) => {
|
||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||
const toolSubtitle =
|
||||
card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||
const isMatch =
|
||||
toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
||||
|
||||
group.classList.toggle('hidden', visibleToolsInCategory === 0);
|
||||
});
|
||||
});
|
||||
|
||||
dom.toolGrid.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const card = e.target.closest('.tool-card');
|
||||
if (card) {
|
||||
const toolId = card.dataset.toolId;
|
||||
setupToolInterface(toolId);
|
||||
card.classList.toggle('hidden', !isMatch);
|
||||
if (isMatch) {
|
||||
visibleToolsInCategory++;
|
||||
}
|
||||
});
|
||||
|
||||
group.classList.toggle('hidden', visibleToolsInCategory === 0);
|
||||
});
|
||||
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
|
||||
dom.alertOkBtn.addEventListener('click', hideAlert);
|
||||
});
|
||||
|
||||
const faqAccordion = document.getElementById('faq-accordion');
|
||||
if (faqAccordion) {
|
||||
faqAccordion.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const questionButton = e.target.closest('.faq-question');
|
||||
if (!questionButton) return;
|
||||
|
||||
const faqItem = questionButton.parentElement;
|
||||
const answer = faqItem.querySelector('.faq-answer');
|
||||
|
||||
faqItem.classList.toggle('open');
|
||||
|
||||
if (faqItem.classList.contains('open')) {
|
||||
answer.style.maxHeight = answer.scrollHeight + 'px';
|
||||
} else {
|
||||
answer.style.maxHeight = '0px';
|
||||
}
|
||||
});
|
||||
dom.toolGrid.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const card = e.target.closest('.tool-card');
|
||||
if (card) {
|
||||
const toolId = card.dataset.toolId;
|
||||
setupToolInterface(toolId);
|
||||
}
|
||||
});
|
||||
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
|
||||
dom.alertOkBtn.addEventListener('click', hideAlert);
|
||||
|
||||
createIcons({ icons });
|
||||
console.log('Please share our tool and share the love!');
|
||||
const faqAccordion = document.getElementById('faq-accordion');
|
||||
if (faqAccordion) {
|
||||
faqAccordion.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const questionButton = e.target.closest('.faq-question');
|
||||
if (!questionButton) return;
|
||||
|
||||
const faqItem = questionButton.parentElement;
|
||||
const answer = faqItem.querySelector('.faq-answer');
|
||||
|
||||
faqItem.classList.toggle('open');
|
||||
|
||||
if (faqItem.classList.contains('open')) {
|
||||
answer.style.maxHeight = answer.scrollHeight + 'px';
|
||||
} else {
|
||||
answer.style.maxHeight = '0px';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
console.log('Please share our tool and share the love!');
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export const state = {
|
||||
activeTool: null,
|
||||
files: [],
|
||||
pdfDoc: null,
|
||||
pdfPages: [],
|
||||
currentPdfUrl: null,
|
||||
activeTool: null,
|
||||
files: [],
|
||||
pdfDoc: null,
|
||||
pdfPages: [],
|
||||
currentPdfUrl: null,
|
||||
};
|
||||
|
||||
// Resets the state when switching views or completing an operation.
|
||||
export function resetState() {
|
||||
state.activeTool = null;
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
state.pdfPages = [];
|
||||
state.currentPdfUrl = null;
|
||||
document.getElementById('tool-content').innerHTML = '';
|
||||
}
|
||||
state.activeTool = null;
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
state.pdfPages = [];
|
||||
state.currentPdfUrl = null;
|
||||
document.getElementById('tool-content').innerHTML = '';
|
||||
}
|
||||
|
||||
501
src/js/ui.ts
501
src/js/ui.ts
@@ -1,107 +1,104 @@
|
||||
import { resetState } from './state.js';
|
||||
import { formatBytes } from './utils/helpers.js';
|
||||
import { tesseractLanguages } from './config/tesseract-languages.js';
|
||||
import { icons, createIcons } from "lucide";
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
// Centralizing DOM element selection
|
||||
export const dom = {
|
||||
gridView: document.getElementById('grid-view'),
|
||||
toolGrid: document.getElementById('tool-grid'),
|
||||
toolInterface: document.getElementById('tool-interface'),
|
||||
toolContent: document.getElementById('tool-content'),
|
||||
backToGridBtn: document.getElementById('back-to-grid'),
|
||||
loaderModal: document.getElementById('loader-modal'),
|
||||
loaderText: document.getElementById('loader-text'),
|
||||
alertModal: document.getElementById('alert-modal'),
|
||||
alertTitle: document.getElementById('alert-title'),
|
||||
alertMessage: document.getElementById('alert-message'),
|
||||
alertOkBtn: document.getElementById('alert-ok'),
|
||||
heroSection: document.getElementById('hero-section'),
|
||||
featuresSection: document.getElementById('features-section'),
|
||||
toolsHeader: document.getElementById('tools-header'),
|
||||
dividers: document.querySelectorAll('.section-divider'),
|
||||
hideSections: document.querySelectorAll('.hide-section'),
|
||||
gridView: document.getElementById('grid-view'),
|
||||
toolGrid: document.getElementById('tool-grid'),
|
||||
toolInterface: document.getElementById('tool-interface'),
|
||||
toolContent: document.getElementById('tool-content'),
|
||||
backToGridBtn: document.getElementById('back-to-grid'),
|
||||
loaderModal: document.getElementById('loader-modal'),
|
||||
loaderText: document.getElementById('loader-text'),
|
||||
alertModal: document.getElementById('alert-modal'),
|
||||
alertTitle: document.getElementById('alert-title'),
|
||||
alertMessage: document.getElementById('alert-message'),
|
||||
alertOkBtn: document.getElementById('alert-ok'),
|
||||
heroSection: document.getElementById('hero-section'),
|
||||
featuresSection: document.getElementById('features-section'),
|
||||
toolsHeader: document.getElementById('tools-header'),
|
||||
dividers: document.querySelectorAll('.section-divider'),
|
||||
hideSections: document.querySelectorAll('.hide-section'),
|
||||
};
|
||||
|
||||
export const showLoader = (text = 'Processing...') => {
|
||||
dom.loaderText.textContent = text;
|
||||
dom.loaderModal.classList.remove('hidden');
|
||||
dom.loaderText.textContent = text;
|
||||
dom.loaderModal.classList.remove('hidden');
|
||||
};
|
||||
|
||||
export const hideLoader = () => dom.loaderModal.classList.add('hidden');
|
||||
|
||||
export const showAlert = (title: any, message: any) => {
|
||||
dom.alertTitle.textContent = title;
|
||||
dom.alertMessage.textContent = message;
|
||||
dom.alertModal.classList.remove('hidden');
|
||||
dom.alertTitle.textContent = title;
|
||||
dom.alertMessage.textContent = message;
|
||||
dom.alertModal.classList.remove('hidden');
|
||||
};
|
||||
|
||||
export const hideAlert = () => dom.alertModal.classList.add('hidden');
|
||||
|
||||
export const switchView = (view: any) => {
|
||||
if (view === 'grid') {
|
||||
dom.gridView.classList.remove('hidden');
|
||||
dom.toolInterface.classList.add('hidden');
|
||||
// show hero and features and header
|
||||
dom.heroSection.classList.remove('hidden');
|
||||
dom.featuresSection.classList.remove('hidden');
|
||||
dom.toolsHeader.classList.remove('hidden');
|
||||
// show dividers
|
||||
dom.dividers.forEach(divider => {
|
||||
divider.classList.remove('hidden');
|
||||
});
|
||||
// show hideSections
|
||||
dom.hideSections.forEach(section => {
|
||||
section.classList.remove('hidden');
|
||||
});
|
||||
if (view === 'grid') {
|
||||
dom.gridView.classList.remove('hidden');
|
||||
dom.toolInterface.classList.add('hidden');
|
||||
// show hero and features and header
|
||||
dom.heroSection.classList.remove('hidden');
|
||||
dom.featuresSection.classList.remove('hidden');
|
||||
dom.toolsHeader.classList.remove('hidden');
|
||||
// show dividers
|
||||
dom.dividers.forEach((divider) => {
|
||||
divider.classList.remove('hidden');
|
||||
});
|
||||
// show hideSections
|
||||
dom.hideSections.forEach((section) => {
|
||||
section.classList.remove('hidden');
|
||||
});
|
||||
|
||||
resetState();
|
||||
} else {
|
||||
dom.gridView.classList.add('hidden');
|
||||
dom.toolInterface.classList.remove('hidden');
|
||||
dom.featuresSection.classList.add('hidden');
|
||||
dom.heroSection.classList.add('hidden');
|
||||
dom.toolsHeader.classList.add('hidden');
|
||||
dom.dividers.forEach(divider => {
|
||||
divider.classList.add('hidden');
|
||||
});
|
||||
dom.hideSections.forEach(section => {
|
||||
section.classList.add('hidden');
|
||||
});
|
||||
|
||||
}
|
||||
resetState();
|
||||
} else {
|
||||
dom.gridView.classList.add('hidden');
|
||||
dom.toolInterface.classList.remove('hidden');
|
||||
dom.featuresSection.classList.add('hidden');
|
||||
dom.heroSection.classList.add('hidden');
|
||||
dom.toolsHeader.classList.add('hidden');
|
||||
dom.dividers.forEach((divider) => {
|
||||
divider.classList.add('hidden');
|
||||
});
|
||||
dom.hideSections.forEach((section) => {
|
||||
section.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const thumbnailState = {
|
||||
sortableInstances: {}
|
||||
sortableInstances: {},
|
||||
};
|
||||
|
||||
|
||||
function initializeOrganizeSortable(containerId: any) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
if (thumbnailState.sortableInstances[containerId]) {
|
||||
thumbnailState.sortableInstances[containerId].destroy();
|
||||
}
|
||||
|
||||
thumbnailState.sortableInstances[containerId] = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.delete-page-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: function(evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function(evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
if (thumbnailState.sortableInstances[containerId]) {
|
||||
thumbnailState.sortableInstances[containerId].destroy();
|
||||
}
|
||||
|
||||
thumbnailState.sortableInstances[containerId] = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.delete-page-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders page thumbnails for tools like 'Organize' and 'Rotate'.
|
||||
@@ -109,97 +106,103 @@ function initializeOrganizeSortable(containerId: any) {
|
||||
* @param {object} pdfDoc The loaded pdf-lib document instance.
|
||||
*/
|
||||
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
showLoader('Rendering page previews...');
|
||||
container.innerHTML = '';
|
||||
showLoader('Rendering page previews...');
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
const pdfData = await pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail relative group';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.pageIndex = i - 1;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail relative group';
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
wrapper.dataset.pageIndex = i - 1;
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className =
|
||||
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
|
||||
imgContainer.appendChild(img);
|
||||
imgContainer.appendChild(img);
|
||||
|
||||
if (toolId === 'organize') {
|
||||
wrapper.className = 'page-thumbnail relative group';
|
||||
wrapper.appendChild(imgContainer);
|
||||
|
||||
const pageNumSpan = document.createElement('span');
|
||||
pageNumSpan.className = 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||
pageNumSpan.textContent = i.toString();
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
|
||||
deleteBtn.innerHTML = '×';
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
(e.currentTarget as HTMLElement).parentElement.remove();
|
||||
initializeOrganizeSortable(containerId);
|
||||
});
|
||||
|
||||
wrapper.append(pageNumSpan, deleteBtn);
|
||||
} else if (toolId === 'rotate') {
|
||||
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
|
||||
wrapper.dataset.rotation = '0';
|
||||
img.classList.add('transition-transform', 'duration-300');
|
||||
wrapper.appendChild(imgContainer);
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
|
||||
|
||||
const pageNumSpan = document.createElement('span');
|
||||
pageNumSpan.className = 'font-medium text-sm text-white';
|
||||
pageNumSpan.textContent = i.toString();
|
||||
|
||||
const rotateBtn = document.createElement('button');
|
||||
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
||||
rotateBtn.title = 'Rotate 90°';
|
||||
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
||||
rotateBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = (e.currentTarget as HTMLElement).closest('.page-rotator-item') as HTMLElement;
|
||||
const imgEl = card.querySelector('img');
|
||||
let currentRotation = parseInt(card.dataset.rotation);
|
||||
currentRotation = (currentRotation + 90) % 360;
|
||||
card.dataset.rotation = currentRotation.toString();
|
||||
imgEl.style.transform = `rotate(${currentRotation}deg)`;
|
||||
});
|
||||
|
||||
controlsDiv.append(pageNumSpan, rotateBtn);
|
||||
wrapper.appendChild(controlsDiv);
|
||||
}
|
||||
|
||||
container.appendChild(wrapper);
|
||||
createIcons({icons});
|
||||
}
|
||||
|
||||
if (toolId === 'organize') {
|
||||
wrapper.className = 'page-thumbnail relative group';
|
||||
wrapper.appendChild(imgContainer);
|
||||
|
||||
const pageNumSpan = document.createElement('span');
|
||||
pageNumSpan.className =
|
||||
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||
pageNumSpan.textContent = i.toString();
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className =
|
||||
'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
|
||||
deleteBtn.innerHTML = '×';
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
(e.currentTarget as HTMLElement).parentElement.remove();
|
||||
initializeOrganizeSortable(containerId);
|
||||
});
|
||||
|
||||
wrapper.append(pageNumSpan, deleteBtn);
|
||||
} else if (toolId === 'rotate') {
|
||||
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
|
||||
wrapper.dataset.rotation = '0';
|
||||
img.classList.add('transition-transform', 'duration-300');
|
||||
wrapper.appendChild(imgContainer);
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
|
||||
|
||||
const pageNumSpan = document.createElement('span');
|
||||
pageNumSpan.className = 'font-medium text-sm text-white';
|
||||
pageNumSpan.textContent = i.toString();
|
||||
|
||||
const rotateBtn = document.createElement('button');
|
||||
rotateBtn.className =
|
||||
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
||||
rotateBtn.title = 'Rotate 90°';
|
||||
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
||||
rotateBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = (e.currentTarget as HTMLElement).closest(
|
||||
'.page-rotator-item'
|
||||
) as HTMLElement;
|
||||
const imgEl = card.querySelector('img');
|
||||
let currentRotation = parseInt(card.dataset.rotation);
|
||||
currentRotation = (currentRotation + 90) % 360;
|
||||
card.dataset.rotation = currentRotation.toString();
|
||||
imgEl.style.transform = `rotate(${currentRotation}deg)`;
|
||||
});
|
||||
|
||||
controlsDiv.append(pageNumSpan, rotateBtn);
|
||||
wrapper.appendChild(controlsDiv);
|
||||
}
|
||||
|
||||
hideLoader();
|
||||
|
||||
container.appendChild(wrapper);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
if (toolId === 'organize') {
|
||||
initializeOrganizeSortable(containerId);
|
||||
}
|
||||
|
||||
hideLoader();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -208,35 +211,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
* @param {File[]} files The array of file objects.
|
||||
*/
|
||||
export const renderFileDisplay = (container: any, files: any) => {
|
||||
container.textContent = '';
|
||||
if (files.length > 0) {
|
||||
files.forEach((file: any) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
container.textContent = '';
|
||||
if (files.length > 0) {
|
||||
files.forEach((file: any) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
|
||||
sizeSpan.textContent = formatBytes(file.size);
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
|
||||
sizeSpan.textContent = formatBytes(file.size);
|
||||
|
||||
fileDiv.append(nameSpan, sizeSpan);
|
||||
container.appendChild(fileDiv);
|
||||
});
|
||||
}
|
||||
fileDiv.append(nameSpan, sizeSpan);
|
||||
container.appendChild(fileDiv);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFileInputHTML = (options = {}) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
|
||||
const multiple = options.multiple ? 'multiple' : '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
|
||||
const acceptedFiles = options.accept || 'application/pdf';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message
|
||||
const showControls = options.showControls || false; // NEW: Add this parameter
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
|
||||
const multiple = options.multiple ? 'multiple' : '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
|
||||
const acceptedFiles = options.accept || 'application/pdf';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message
|
||||
const showControls = options.showControls || false; // NEW: Add this parameter
|
||||
|
||||
return `
|
||||
return `
|
||||
<div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
|
||||
@@ -247,7 +251,9 @@ const createFileInputHTML = (options = {}) => {
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
|
||||
</div>
|
||||
|
||||
${showControls ? `
|
||||
${
|
||||
showControls
|
||||
? `
|
||||
<!-- NEW: Add control buttons for multi-file uploads -->
|
||||
<div id="file-controls" class="hidden mt-4 flex gap-3">
|
||||
<button id="add-more-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
@@ -257,12 +263,14 @@ const createFileInputHTML = (options = {}) => {
|
||||
<i data-lucide="x"></i> Clear All
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export const toolTemplates = {
|
||||
merge: () => `
|
||||
merge: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Merge PDFs</h2>
|
||||
<p class="mb-6 text-gray-400">Combine whole files, or select specific pages to merge into a new document.</p>
|
||||
${createFileInputHTML({ multiple: true, showControls: true })}
|
||||
@@ -300,7 +308,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
`,
|
||||
|
||||
split: () => `
|
||||
split: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Split PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Extract pages from a PDF using various methods.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -370,7 +378,7 @@ export const toolTemplates = {
|
||||
|
||||
</div>
|
||||
`,
|
||||
encrypt: () => `
|
||||
encrypt: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to create a new, password-protected version.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -388,7 +396,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
<canvas id="pdf-canvas" class="hidden"></canvas>
|
||||
`,
|
||||
decrypt: () => `
|
||||
decrypt: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Decrypt PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Upload an encrypted PDF and provide its password to create an unlocked version.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -402,7 +410,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
<canvas id="pdf-canvas" class="hidden"></canvas>
|
||||
`,
|
||||
organize: () => `
|
||||
organize: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Organize PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Reorder, rotate, or delete pages. Drag and drop pages to reorder them.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -411,7 +419,7 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Save Changes</button>
|
||||
`,
|
||||
|
||||
rotate: () => `
|
||||
rotate: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Rotate PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Rotate all or specific pages in a PDF document.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -436,7 +444,7 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Save Rotations</button>
|
||||
`,
|
||||
|
||||
'add-page-numbers': () => `
|
||||
'add-page-numbers': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Add Page Numbers</h2>
|
||||
<p class="mb-6 text-gray-400">Add customizable page numbers to your PDF file.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -471,7 +479,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Add Page Numbers</button>
|
||||
`,
|
||||
'pdf-to-jpg': () => `
|
||||
'pdf-to-jpg': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to JPG</h2>
|
||||
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality JPG image.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -481,14 +489,14 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Download All as ZIP</button>
|
||||
</div>
|
||||
`,
|
||||
'jpg-to-pdf': () => `
|
||||
'jpg-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">JPG to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more JPG images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'scan-to-pdf': () => `
|
||||
'scan-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Scan to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Use your device's camera to scan documents and save them as a PDF. On desktop, this will open a file picker.</p>
|
||||
${createFileInputHTML({ accept: 'image/*' })}
|
||||
@@ -496,7 +504,7 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF from Scans</button>
|
||||
`,
|
||||
|
||||
crop: () => `
|
||||
crop: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Crop PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Click and drag to select a crop area on any page. You can set different crop areas for each page.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -521,7 +529,7 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Apply Crop & Save PDF</button>
|
||||
</div>
|
||||
`,
|
||||
compress: () => `
|
||||
compress: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -551,14 +559,14 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4" disabled>Compress PDF</button>
|
||||
</div>
|
||||
`,
|
||||
'pdf-to-greyscale': () => `
|
||||
'pdf-to-greyscale': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to Greyscale</h2>
|
||||
<p class="mb-6 text-gray-400">Convert all pages of a PDF to greyscale. This is done by rendering each page, applying a filter, and rebuilding the PDF.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to Greyscale</button>
|
||||
`,
|
||||
'pdf-to-zip': () => `
|
||||
'pdf-to-zip': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Combine PDFs into ZIP</h2>
|
||||
<p class="mb-6 text-gray-400">Select multiple PDF files to download them together in a single ZIP archive.</p>
|
||||
${createFileInputHTML({ multiple: true, showControls: true })}
|
||||
@@ -566,7 +574,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Create ZIP File</button>
|
||||
`,
|
||||
|
||||
'edit-metadata': () => `
|
||||
'edit-metadata': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Edit PDF Metadata</h2>
|
||||
<p class="mb-6 text-gray-400">Modify the core metadata fields of your PDF. Leave a field blank to clear it.</p>
|
||||
|
||||
@@ -629,21 +637,21 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Update Metadata & Download</button>
|
||||
`,
|
||||
|
||||
'remove-metadata': () => `
|
||||
'remove-metadata': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Remove PDF Metadata</h2>
|
||||
<p class="mb-6 text-gray-400">Completely remove identifying metadata from your PDF.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Remove Metadata & Download</button>
|
||||
`,
|
||||
flatten: () => `
|
||||
flatten: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
|
||||
`,
|
||||
'pdf-to-png': () => `
|
||||
'pdf-to-png': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to PNG</h2>
|
||||
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality PNG image.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -653,14 +661,14 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
|
||||
</div>
|
||||
`,
|
||||
'png-to-pdf': () => `
|
||||
'png-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PNG to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more PNG images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'pdf-to-webp': () => `
|
||||
'pdf-to-webp': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to WebP</h2>
|
||||
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a modern WebP image.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -670,14 +678,14 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
|
||||
</div>
|
||||
`,
|
||||
'webp-to-pdf': () => `
|
||||
'webp-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">WebP to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more WebP images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
edit: () => `
|
||||
edit: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF Studio</h2>
|
||||
<p class="mb-6 text-gray-400">An all-in-one PDF workspace where you can annotate, draw, highlight, redact, add comments and shapes, take screenshots, and view PDFs.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -686,7 +694,7 @@ compress: () => `
|
||||
<div id="embed-pdf-container" class="w-full h-full"></div>
|
||||
</div>
|
||||
`,
|
||||
'delete-pages': () => `
|
||||
'delete-pages': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Delete Pages</h2>
|
||||
<p class="mb-6 text-gray-400">Remove specific pages or ranges of pages from your PDF file.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -698,7 +706,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
|
||||
</div>
|
||||
`,
|
||||
'add-blank-page': () => `
|
||||
'add-blank-page': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Add Blank Page</h2>
|
||||
<p class="mb-6 text-gray-400">Insert a new blank page at a specific position in your document.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -710,7 +718,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full">Add Page & Download</button>
|
||||
</div>
|
||||
`,
|
||||
'extract-pages': () => `
|
||||
'extract-pages': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Extract Pages</h2>
|
||||
<p class="mb-6 text-gray-400">Extract specific pages from a PDF into separate files. Your files will download in a ZIP archive.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -723,7 +731,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'add-watermark': () => `
|
||||
'add-watermark': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Add Watermark</h2>
|
||||
<p class="mb-6 text-gray-400">Apply a text or image watermark to every page of your PDF document.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -787,8 +795,7 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Add Watermark & Download</button>
|
||||
`,
|
||||
|
||||
|
||||
'add-header-footer': () => `
|
||||
'add-header-footer': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Add Header & Footer</h2>
|
||||
<p class="mb-6 text-gray-400">Add custom text to the top and bottom margins of every page.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -846,8 +853,7 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Header & Footer</button>
|
||||
`,
|
||||
|
||||
|
||||
'image-to-pdf': () => `
|
||||
'image-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
|
||||
<p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}
|
||||
@@ -855,7 +861,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
|
||||
'change-permissions': () => `
|
||||
'change-permissions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
|
||||
<p class="mb-6 text-gray-400">Unlock a PDF and re-save it with new passwords and permissions.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -903,8 +909,7 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Save New Permissions</button>
|
||||
`,
|
||||
|
||||
|
||||
'pdf-to-markdown': () => `
|
||||
'pdf-to-markdown': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to Markdown</h2>
|
||||
<p class="mb-6 text-gray-400">Convert a PDF's text content into a structured Markdown file.</p>
|
||||
${createFileInputHTML({ accept: '.pdf' })}
|
||||
@@ -914,7 +919,7 @@ compress: () => `
|
||||
</div>
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Convert to Markdown</button>
|
||||
`,
|
||||
'txt-to-pdf': () => `
|
||||
'txt-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Text to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Type or paste your text below and convert it to a PDF with custom formatting.</p>
|
||||
<textarea id="text-input" rows="12" class="w-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-2.5 font-sans" placeholder="Start typing here..."></textarea>
|
||||
@@ -945,28 +950,28 @@ compress: () => `
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF</button>
|
||||
`,
|
||||
'invert-colors': () => `
|
||||
'invert-colors': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Invert PDF Colors</h2>
|
||||
<p class="mb-6 text-gray-400">Convert your PDF to a "dark mode" by inverting its colors. This works best on simple text and image documents.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Invert Colors & Download</button>
|
||||
`,
|
||||
'view-metadata': () => `
|
||||
'view-metadata': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">View PDF Metadata</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to view its internal properties, such as Title, Author, and Creation Date.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="metadata-results" class="hidden mt-6 p-4 bg-gray-900 border border-gray-700 rounded-lg"></div>
|
||||
`,
|
||||
'reverse-pages': () => `
|
||||
'reverse-pages': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2>
|
||||
<p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
|
||||
`,
|
||||
'md-to-pdf': () => `
|
||||
'md-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Markdown to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Write in Markdown, select your formatting options, and get a high-quality, multi-page PDF. <br><strong class="text-gray-300">Note:</strong> Images linked from the web (e.g., https://...) require an internet connection to be rendered.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
@@ -999,42 +1004,42 @@ compress: () => `
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF from Markdown</button>
|
||||
`,
|
||||
'svg-to-pdf': () => `
|
||||
'svg-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">SVG to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more SVG vector images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'bmp-to-pdf': () => `
|
||||
'bmp-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">BMP to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more BMP images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/bmp', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'heic-to-pdf': () => `
|
||||
'heic-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">HEIC to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more HEIC (High Efficiency) images from your iPhone or camera into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: '.heic,.heif', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'tiff-to-pdf': () => `
|
||||
'tiff-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">TIFF to PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert one or more single or multi-page TIFF images into a single PDF file.</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'image/tiff', showControls: true })}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
|
||||
`,
|
||||
'pdf-to-bmp': () => `
|
||||
'pdf-to-bmp': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to BMP</h2>
|
||||
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a BMP image. Your files will be downloaded in a ZIP archive.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to BMP & Download ZIP</button>
|
||||
`,
|
||||
'pdf-to-tiff': () => `
|
||||
'pdf-to-tiff': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to TIFF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality TIFF image. Your files will be downloaded in a ZIP archive.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1042,7 +1047,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to TIFF & Download ZIP</button>
|
||||
`,
|
||||
|
||||
'split-in-half': () => `
|
||||
'split-in-half': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Split Pages in Half</h2>
|
||||
<p class="mb-6 text-gray-400">Choose a method to divide every page of your document into two separate pages.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1058,7 +1063,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Split PDF</button>
|
||||
</div>
|
||||
`,
|
||||
'page-dimensions': () => `
|
||||
'page-dimensions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Analyze Page Dimensions</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to see the precise dimensions, standard size, and orientation of every page.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1091,7 +1096,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'n-up': () => `
|
||||
'n-up': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">N-Up Page Arrangement</h2>
|
||||
<p class="mb-6 text-gray-400">Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1154,7 +1159,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'duplicate-organize': () => `
|
||||
'duplicate-organize': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Page Manager</h2>
|
||||
<p class="mb-6 text-gray-400">Drag pages to reorder them. Use the <i data-lucide="copy-plus" class="inline-block w-4 h-4 text-green-400"></i> icon to duplicate a page or the <i data-lucide="x-circle" class="inline-block w-4 h-4 text-red-400"></i> icon to delete it.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1167,7 +1172,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'combine-single-page': () => `
|
||||
'combine-single-page': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Combine to a Single Page</h2>
|
||||
<p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically to create one continuous, scrollable page.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1194,7 +1199,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'fix-dimensions': () => `
|
||||
'fix-dimensions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Standardize Page Dimensions</h2>
|
||||
<p class="mb-6 text-gray-400">Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1270,7 +1275,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'change-background-color': () => `
|
||||
'change-background-color': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Change Background Color</h2>
|
||||
<p class="mb-6 text-gray-400">Select a new background color for every page of your PDF.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1282,7 +1287,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'change-text-color': () => `
|
||||
'change-text-color': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Change Text Color</h2>
|
||||
<p class="mb-6 text-gray-400">Change the color of dark text in your PDF. This process converts pages to images, so text will not be selectable in the final file.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1306,7 +1311,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'compare-pdfs': () => `
|
||||
'compare-pdfs': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Compare PDFs</h2>
|
||||
<p class="mb-6 text-gray-400">Upload two files to visually compare them using either an overlay or a side-by-side view.</p>
|
||||
|
||||
@@ -1357,7 +1362,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'ocr-pdf': () => `
|
||||
'ocr-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1369,12 +1374,16 @@ compress: () => `
|
||||
<div class="relative">
|
||||
<input type="text" id="lang-search" class="w-full bg-gray-900 border border-gray-600 text-white rounded-lg p-2.5 mb-2" placeholder="Search for languages...">
|
||||
<div id="lang-list" class="max-h-48 overflow-y-auto border border-gray-600 rounded-lg p-2 bg-gray-900">
|
||||
${Object.entries(tesseractLanguages).map(([code, name]) => `
|
||||
${Object.entries(tesseractLanguages)
|
||||
.map(
|
||||
([code, name]) => `
|
||||
<label class="flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" value="${code}" class="lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
${name}
|
||||
</label>
|
||||
`).join('')}
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Selected: <span id="selected-langs-display" class="font-semibold">None</span></p>
|
||||
@@ -1433,7 +1442,7 @@ compress: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'word-to-pdf': () => `
|
||||
'word-to-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Word to PDF Converter</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a .docx file to convert it into a high-quality PDF with selectable text. Complex layouts may not be perfectly preserved.</p>
|
||||
|
||||
@@ -1452,7 +1461,7 @@ compress: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Preview & Convert</button>
|
||||
`,
|
||||
|
||||
'sign-pdf': () => `
|
||||
'sign-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Sign PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Create your signature, select it, then click on the document to place. You can drag to move placed signatures.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1543,7 +1552,7 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Signatures & Download PDF</button>
|
||||
`,
|
||||
|
||||
'remove-annotations': () => `
|
||||
'remove-annotations': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Remove Annotations</h2>
|
||||
<p class="mb-6 text-gray-400">Select the types of annotations to remove from all pages or a specific range.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1602,8 +1611,7 @@ compress: () => `
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Remove Selected Annotations</button>
|
||||
`,
|
||||
|
||||
|
||||
cropper: () => `
|
||||
cropper: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF Cropper</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.</p>
|
||||
|
||||
@@ -1647,7 +1655,7 @@ cropper: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'form-filler': () => `
|
||||
'form-filler': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">PDF Form Filler</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to fill in existing form fields. The PDF view on the right will update as you type.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1694,7 +1702,7 @@ cropper: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
posterize: () => `
|
||||
posterize: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Posterize PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1794,7 +1802,7 @@ posterize: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'remove-blank-pages': () => `
|
||||
'remove-blank-pages': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Remove Blank Pages</h2>
|
||||
<p class="mb-6 text-gray-400">Automatically detect and remove blank or nearly blank pages from your PDF. Adjust the sensitivity to control what is considered "blank".</p>
|
||||
${createFileInputHTML()}
|
||||
@@ -1819,7 +1827,7 @@ posterize: () => `
|
||||
</div>
|
||||
`,
|
||||
|
||||
'alternate-merge': () => `
|
||||
'alternate-merge': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Alternate & Mix Pages</h2>
|
||||
<p class="mb-6 text-gray-400">Combine pages from 2 or more documents, alternating between them. Drag the files to set the mixing order (e.g., Page 1 from Doc A, Page 1 from Doc B, Page 2 from Doc A, Page 2 from Doc B, etc.).</p>
|
||||
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
|
||||
@@ -1836,5 +1844,4 @@ posterize: () => `
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Alternate & Mix PDFs</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,114 +1,125 @@
|
||||
const STANDARD_SIZES = {
|
||||
'A4': { width: 595.28, height: 841.89 },
|
||||
'Letter': { width: 612, height: 792 },
|
||||
'Legal': { width: 612, height: 1008 },
|
||||
'Tabloid': { width: 792, height: 1224 },
|
||||
'A3': { width: 841.89, height: 1190.55 },
|
||||
'A5': { width: 419.53, height: 595.28 },
|
||||
A4: { width: 595.28, height: 841.89 },
|
||||
Letter: { width: 612, height: 792 },
|
||||
Legal: { width: 612, height: 1008 },
|
||||
Tabloid: { width: 792, height: 1224 },
|
||||
A3: { width: 841.89, height: 1190.55 },
|
||||
A5: { width: 419.53, height: 595.28 },
|
||||
};
|
||||
|
||||
export function getStandardPageName(width: any, height: any) {
|
||||
const tolerance = 1; // Allow for minor floating point variations
|
||||
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
||||
if ((Math.abs(width - size.width) < tolerance && Math.abs(height - size.height) < tolerance) ||
|
||||
(Math.abs(width - size.height) < tolerance && Math.abs(height - size.width) < tolerance)) {
|
||||
return name;
|
||||
}
|
||||
const tolerance = 1; // Allow for minor floating point variations
|
||||
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
||||
if (
|
||||
(Math.abs(width - size.width) < tolerance &&
|
||||
Math.abs(height - size.height) < tolerance) ||
|
||||
(Math.abs(width - size.height) < tolerance &&
|
||||
Math.abs(height - size.width) < tolerance)
|
||||
) {
|
||||
return name;
|
||||
}
|
||||
return 'Custom';
|
||||
}
|
||||
return 'Custom';
|
||||
}
|
||||
|
||||
export function convertPoints(points: any, unit: any) {
|
||||
let result = 0;
|
||||
switch (unit) {
|
||||
case 'in':
|
||||
result = points / 72;
|
||||
break;
|
||||
case 'mm':
|
||||
result = (points / 72) * 25.4;
|
||||
break;
|
||||
case 'px':
|
||||
result = points * (96 / 72); // Assuming 96 DPI
|
||||
break;
|
||||
default: // 'pt'
|
||||
result = points;
|
||||
break;
|
||||
}
|
||||
return result.toFixed(2);
|
||||
let result = 0;
|
||||
switch (unit) {
|
||||
case 'in':
|
||||
result = points / 72;
|
||||
break;
|
||||
case 'mm':
|
||||
result = (points / 72) * 25.4;
|
||||
break;
|
||||
case 'px':
|
||||
result = points * (96 / 72); // Assuming 96 DPI
|
||||
break;
|
||||
default: // 'pt'
|
||||
result = points;
|
||||
break;
|
||||
}
|
||||
return result.toFixed(2);
|
||||
}
|
||||
|
||||
export const hexToRgb = (hex: any) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255,
|
||||
} : { r: 0, g: 0, b: 0 }; // Default to black
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 }; // Default to black
|
||||
};
|
||||
|
||||
export const formatBytes = (bytes: any, decimals = 1) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
export const downloadFile = (blob: any, filename: any) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const readFileAsArrayBuffer = (file: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
export function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
if (!rangeString || rangeString.trim() === '') {
|
||||
return Array.from({ length: totalPages }, (_, i) => i);
|
||||
if (!rangeString || rangeString.trim() === '') {
|
||||
return Array.from({ length: totalPages }, (_, i) => i);
|
||||
}
|
||||
|
||||
const indices = new Set();
|
||||
const parts = rangeString.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const trimmedPart = part.trim();
|
||||
if (!trimmedPart) continue;
|
||||
|
||||
if (trimmedPart.includes('-')) {
|
||||
const [start, end] = trimmedPart.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
) {
|
||||
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedPart);
|
||||
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
||||
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const indices = new Set();
|
||||
const parts = rangeString.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const trimmedPart = part.trim();
|
||||
if (!trimmedPart) continue;
|
||||
|
||||
if (trimmedPart.includes('-')) {
|
||||
const [start, end] = trimmedPart.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) {
|
||||
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedPart);
|
||||
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
||||
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import Sortable from 'sortablejs';
|
||||
import * as helpers from '@/js/utils/helpers';
|
||||
import * as ui from '@/js/ui';
|
||||
import { setupAlternateMergeTool, alternateMerge } from '@/js/logic/alternate-merge';
|
||||
import {
|
||||
setupAlternateMergeTool,
|
||||
alternateMerge,
|
||||
} from '@/js/logic/alternate-merge';
|
||||
|
||||
vi.mock('pdf-lib', () => ({
|
||||
PDFDocument: {
|
||||
@@ -47,7 +50,9 @@ describe('Alternate Merge Tool', () => {
|
||||
mockPdfDoc1 = { getPageCount: vi.fn(() => 2) };
|
||||
mockPdfDoc2 = { getPageCount: vi.fn(() => 3) };
|
||||
|
||||
vi.mocked(helpers.readFileAsArrayBuffer).mockResolvedValue(new ArrayBuffer(8));
|
||||
vi.mocked(helpers.readFileAsArrayBuffer).mockResolvedValue(
|
||||
new ArrayBuffer(8)
|
||||
);
|
||||
vi.mocked(PDFDocument.load)
|
||||
.mockResolvedValueOnce(mockPdfDoc1)
|
||||
.mockResolvedValueOnce(mockPdfDoc2);
|
||||
@@ -66,16 +71,18 @@ describe('Alternate Merge Tool', () => {
|
||||
expect(ui.showLoader).toHaveBeenCalledWith('Loading PDF documents...');
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
expect(PDFDocument.load).toHaveBeenCalledTimes(2);
|
||||
expect(document.querySelectorAll('#alternate-file-list li').length).toBe(2);
|
||||
expect(document.querySelectorAll('#alternate-file-list li').length).toBe(
|
||||
2
|
||||
);
|
||||
expect(Sortable.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show alert on load failure', async () => {
|
||||
vi.mocked(PDFDocument.load).mockReset();
|
||||
vi.mocked(PDFDocument.load).mockRejectedValueOnce(new Error('bad pdf'));
|
||||
|
||||
|
||||
await setupAlternateMergeTool();
|
||||
|
||||
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
expect.stringContaining('Failed to load one or more PDF files')
|
||||
@@ -90,12 +97,12 @@ describe('Alternate Merge Tool', () => {
|
||||
state.files = [new File(['dummy1'], 'file1.pdf')];
|
||||
vi.mocked(PDFDocument.load).mockReset();
|
||||
vi.mocked(PDFDocument.load).mockResolvedValueOnce(mockPdfDoc1);
|
||||
|
||||
|
||||
await setupAlternateMergeTool();
|
||||
vi.clearAllMocks(); // Clear the setup calls
|
||||
|
||||
|
||||
await alternateMerge();
|
||||
|
||||
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Not Enough Files',
|
||||
expect.stringContaining('Please upload at least two PDF files')
|
||||
@@ -106,7 +113,7 @@ describe('Alternate Merge Tool', () => {
|
||||
// First setup the tool to populate internal state
|
||||
await setupAlternateMergeTool();
|
||||
vi.clearAllMocks(); // Clear setup calls
|
||||
|
||||
|
||||
const mockCopyPages = vi.fn(() =>
|
||||
Promise.resolve([{ page: 'mockPage' }] as any)
|
||||
);
|
||||
@@ -129,12 +136,17 @@ describe('Alternate Merge Tool', () => {
|
||||
|
||||
await alternateMerge();
|
||||
|
||||
expect(ui.showLoader).toHaveBeenCalledWith(expect.stringContaining('Alternating'));
|
||||
expect(ui.showLoader).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Alternating')
|
||||
);
|
||||
expect(mockCopyPages).toHaveBeenCalled();
|
||||
expect(mockAddPage).toHaveBeenCalled();
|
||||
expect(mockSave).toHaveBeenCalled();
|
||||
expect(helpers.downloadFile).toHaveBeenCalled();
|
||||
expect(ui.showAlert).toHaveBeenCalledWith('Success', expect.stringContaining('mixed successfully'));
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Success',
|
||||
expect.stringContaining('mixed successfully')
|
||||
);
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -142,7 +154,7 @@ describe('Alternate Merge Tool', () => {
|
||||
// Setup the tool first to populate internal state with 2 PDFs
|
||||
await setupAlternateMergeTool();
|
||||
vi.clearAllMocks(); // Clear setup calls
|
||||
|
||||
|
||||
// Mock PDFDocument.create to reject
|
||||
vi.mocked(PDFDocument.create).mockRejectedValue(new Error('broken'));
|
||||
|
||||
@@ -155,4 +167,4 @@ describe('Alternate Merge Tool', () => {
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,4 +211,4 @@ describe('helpers', () => {
|
||||
expect(result).toEqual([0, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { singlePdfLoadTools, simpleTools, multiFileTools } from '@/js/config/pdf-tools';
|
||||
import {
|
||||
singlePdfLoadTools,
|
||||
simpleTools,
|
||||
multiFileTools,
|
||||
} from '@/js/config/pdf-tools';
|
||||
|
||||
describe('Tool Configuration Arrays', () => {
|
||||
|
||||
// --- Tests for singlePdfLoadTools ---
|
||||
describe('singlePdfLoadTools', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
@@ -30,7 +33,7 @@ describe('Tool Configuration Arrays', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
expect(Array.isArray(simpleTools)).toBe(true);
|
||||
expect(simpleTools.length).toBeGreaterThan(0);
|
||||
simpleTools.forEach(tool => {
|
||||
simpleTools.forEach((tool) => {
|
||||
expect(typeof tool).toBe('string');
|
||||
expect(tool.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -51,7 +54,7 @@ describe('Tool Configuration Arrays', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
expect(Array.isArray(multiFileTools)).toBe(true);
|
||||
expect(multiFileTools.length).toBeGreaterThan(0);
|
||||
multiFileTools.forEach(tool => {
|
||||
multiFileTools.forEach((tool) => {
|
||||
expect(typeof tool).toBe('string');
|
||||
expect(tool.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -82,5 +85,4 @@ describe('Tool Configuration Arrays', () => {
|
||||
expect(uniqueTools.size).toBe(allTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { setupRemoveBlankPagesTool, removeBlankPages } from '@/js/logic/remove-blank-pages';
|
||||
import {
|
||||
setupRemoveBlankPagesTool,
|
||||
removeBlankPages,
|
||||
} from '@/js/logic/remove-blank-pages';
|
||||
import * as ui from '@/js/ui';
|
||||
import * as helpers from '@/js/utils/helpers';
|
||||
import { state } from '@/js/state';
|
||||
@@ -11,7 +14,12 @@ if (typeof ImageData === 'undefined') {
|
||||
width: number;
|
||||
height: number;
|
||||
colorSpace: string;
|
||||
constructor(data: Uint8ClampedArray, width: number, height: number, options?: { colorSpace: string }) {
|
||||
constructor(
|
||||
data: Uint8ClampedArray,
|
||||
width: number,
|
||||
height: number,
|
||||
options?: { colorSpace: string }
|
||||
) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
@@ -27,11 +35,17 @@ const mockContext: CanvasRenderingContext2D = {
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(mockContext);
|
||||
HTMLCanvasElement.prototype.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,mock');
|
||||
HTMLCanvasElement.prototype.toDataURL = vi
|
||||
.fn()
|
||||
.mockReturnValue('data:image/png;base64,mock');
|
||||
|
||||
function createMockPage(isBlank: boolean) {
|
||||
return {
|
||||
getViewport: vi.fn(({ scale }) => ({ width: 800 * scale, height: 600 * scale, scale })),
|
||||
getViewport: vi.fn(({ scale }) => ({
|
||||
width: 800 * scale,
|
||||
height: 600 * scale,
|
||||
scale,
|
||||
})),
|
||||
render: vi.fn(() => {
|
||||
// Return ImageData depending on blank/content
|
||||
mockContext.getImageData = vi.fn(() => {
|
||||
@@ -187,4 +201,4 @@ describe('Remove Blank Pages Tool', () => {
|
||||
);
|
||||
expect(helpers.downloadFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
@@ -29,4 +29,4 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,6 @@ import { state, resetState } from '@/js/state';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
describe('State Management', () => {
|
||||
|
||||
// Test the initial state on import
|
||||
describe('Initial State', () => {
|
||||
it('should have the correct initial values', () => {
|
||||
@@ -16,7 +15,6 @@ describe('State Management', () => {
|
||||
|
||||
// Test the resetState function
|
||||
describe('resetState function', () => {
|
||||
|
||||
// Before each test in this block, we'll "dirty" the state
|
||||
// to ensure the reset function is actually doing something.
|
||||
beforeEach(() => {
|
||||
@@ -29,7 +27,8 @@ describe('State Management', () => {
|
||||
|
||||
// 2. Create the DOM element that the function interacts with
|
||||
// The setup.ts file will clean this up automatically after each test.
|
||||
document.body.innerHTML = '<div id="tool-content">Some old tool content</div>';
|
||||
document.body.innerHTML =
|
||||
'<div id="tool-content">Some old tool content</div>';
|
||||
});
|
||||
|
||||
it('should reset all state properties to their initial values', () => {
|
||||
@@ -57,4 +56,4 @@ describe('State Management', () => {
|
||||
expect(toolContentElement?.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { categories } from '@/js/config/tools';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Tool Categories Configuration', () => {
|
||||
|
||||
// 1. Basic Structure and Type Checking
|
||||
it('should be an array of category objects', () => {
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
@@ -13,7 +12,6 @@ describe('Tool Categories Configuration', () => {
|
||||
|
||||
// 2. Loop through each category to perform specific checks
|
||||
describe.each(categories)('Category: "$name"', (category) => {
|
||||
|
||||
// Check that the category object itself is well-formed
|
||||
it('should have a non-empty "name" string and a non-empty "tools" array', () => {
|
||||
expect(typeof category.name).toBe('string');
|
||||
@@ -24,7 +22,7 @@ describe('Tool Categories Configuration', () => {
|
||||
|
||||
// **KEY CHANGE**: This test now ensures IDs are unique only WITHIN this specific category.
|
||||
it('should not contain any duplicate tool IDs within its own list', () => {
|
||||
const toolIds = category.tools.map(tool => tool.id);
|
||||
const toolIds = category.tools.map((tool) => tool.id);
|
||||
const uniqueToolIds = new Set(toolIds);
|
||||
|
||||
// This assertion checks for duplicates inside THIS category only.
|
||||
@@ -49,4 +47,4 @@ describe('Tool Categories Configuration', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user