feat: Add VitePress docs, EPUB to PDF tool, Phosphor icons, and licensing updates
- Set up VitePress documentation site (docs:dev, docs:build, docs:preview) - Added Getting Started, Tools Reference, Contributing, and Commercial License pages - Created self-hosting guides for Docker, Vercel, Netlify, Cloudflare, AWS, Hostinger, Nginx, Apache - Updated README with documentation link, sponsors section, and docs contribution guide - Added EPUB to PDF converter using LibreOffice WASM - Migrated to Phosphor Icons for consistent iconography - Added donation ribbon banner on landing page - Removed 'Like My Work?' section (replaced by ribbon) - Updated licensing.html with delivery model, AGPL notice, invoicing, and no-refund policy - Added Commercial License documentation page - Updated translations table (Chinese added, marked non-English as In Progress) - Added sponsors.yml workflow for auto-generating sponsor avatars
This commit is contained in:
741
src/css/markdown-editor.css
Normal file
741
src/css/markdown-editor.css
Normal file
@@ -0,0 +1,741 @@
|
||||
.md-editor-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.md-editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.md-editor-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.md-editor-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.md-editor-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.md-editor-btn-secondary {
|
||||
color: rgb(156, 163, 175);
|
||||
background: rgb(31, 41, 55);
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
}
|
||||
|
||||
.md-editor-btn-secondary:hover {
|
||||
background: rgb(55, 65, 81);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-btn-primary {
|
||||
color: white;
|
||||
background: rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
.md-editor-btn-primary:hover {
|
||||
background: rgb(99, 102, 241);
|
||||
}
|
||||
|
||||
.md-editor-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: rgb(31, 41, 55);
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(156, 163, 175);
|
||||
}
|
||||
|
||||
.theme-toggle-slider {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background: rgb(55, 65, 81);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle-slider.active {
|
||||
background: rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
.theme-toggle-slider.active::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Split View Container */
|
||||
.md-editor-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
height: 600px;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.md-editor-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: rgb(31, 41, 55);
|
||||
}
|
||||
|
||||
.md-editor-pane-header {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgb(156, 163, 175);
|
||||
background: rgb(17, 24, 39);
|
||||
border-bottom: 1px solid rgb(55, 65, 81);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Separator between panes */
|
||||
.md-editor-pane:first-child {
|
||||
border-right: 2px solid rgb(55, 65, 81);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-pane:first-child {
|
||||
border-right-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
/* Editor Textarea */
|
||||
.md-editor-textarea {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: rgb(229, 231, 235);
|
||||
background: rgb(31, 41, 55);
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.md-editor-textarea::placeholder {
|
||||
color: rgb(107, 114, 128);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-pane {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-pane-header {
|
||||
background: rgb(249, 250, 251);
|
||||
color: rgb(75, 85, 99);
|
||||
border-bottom-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-textarea {
|
||||
background: white;
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview {
|
||||
background: white;
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor-preview {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
background: rgb(31, 41, 55);
|
||||
color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor-preview h1,
|
||||
.md-editor-preview h2,
|
||||
.md-editor-preview h3,
|
||||
.md-editor-preview h4,
|
||||
.md-editor-preview h5,
|
||||
.md-editor-preview h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-preview h1:first-child,
|
||||
.md-editor-preview h2:first-child,
|
||||
.md-editor-preview h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.md-editor-preview h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid rgb(55, 65, 81);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.md-editor-preview h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid rgb(55, 65, 81);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.md-editor-preview h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.md-editor-preview p {
|
||||
margin: 1em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.md-editor-preview a {
|
||||
color: rgb(96, 165, 250);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.md-editor-preview a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.md-editor-preview code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
background: rgb(17, 24, 39);
|
||||
color: rgb(251, 191, 36);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.md-editor-preview pre {
|
||||
background: rgb(17, 24, 39);
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
border-radius: 0.5rem;
|
||||
line-height: 1.45;
|
||||
margin: 1em 0;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
}
|
||||
|
||||
.md-editor-preview pre code {
|
||||
background: none;
|
||||
color: rgb(229, 231, 235);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-editor-preview blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0 1em;
|
||||
color: rgb(156, 163, 175);
|
||||
border-left: 4px solid rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
.md-editor-preview ul,
|
||||
.md-editor-preview ol {
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.md-editor-preview li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.md-editor-preview table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.md-editor-preview th,
|
||||
.md-editor-preview td {
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.md-editor-preview th {
|
||||
background: rgb(17, 24, 39);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-preview tr:nth-child(even) {
|
||||
background: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor-preview hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(55, 65, 81);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.md-editor-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Light Mode Preview Styles */
|
||||
.md-editor.light-mode .md-editor-preview h1,
|
||||
.md-editor.light-mode .md-editor-preview h2,
|
||||
.md-editor.light-mode .md-editor-preview h3,
|
||||
.md-editor.light-mode .md-editor-preview h4,
|
||||
.md-editor.light-mode .md-editor-preview h5,
|
||||
.md-editor.light-mode .md-editor-preview h6 {
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview h1,
|
||||
.md-editor.light-mode .md-editor-preview h2 {
|
||||
border-bottom-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview a {
|
||||
color: rgb(29, 78, 216);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview code {
|
||||
background: rgb(249, 250, 251);
|
||||
color: rgb(220, 38, 38);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview pre {
|
||||
background: rgb(249, 250, 251);
|
||||
border-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview pre code {
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview blockquote {
|
||||
color: rgb(75, 85, 99);
|
||||
border-left-color: rgb(99, 102, 241);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview th,
|
||||
.md-editor.light-mode .md-editor-preview td {
|
||||
border-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview th {
|
||||
background: rgb(249, 250, 251);
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview tr:nth-child(even) {
|
||||
background: rgb(249, 250, 251);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview hr {
|
||||
border-top-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
/* Markdown Plugin Styles */
|
||||
.md-editor-preview mark {
|
||||
background-color: rgb(254, 243, 199);
|
||||
color: rgb(17, 24, 39);
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.md-editor-preview ins {
|
||||
text-decoration: none;
|
||||
background-color: rgb(134, 239, 172);
|
||||
color: rgb(17, 24, 39);
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.md-editor-preview .task-list-item {
|
||||
list-style-type: none;
|
||||
margin-left: -1.5em;
|
||||
}
|
||||
|
||||
.md-editor-preview .footnotes {
|
||||
margin-top: 2em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid rgb(55, 65, 81);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview .footnotes {
|
||||
border-top-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor-preview .table-of-contents {
|
||||
background: rgb(17, 24, 39);
|
||||
padding: 1em 1.5em;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1em 0;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-preview .table-of-contents {
|
||||
background: rgb(249, 250, 251);
|
||||
border-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
/* Settings Modal */
|
||||
.md-editor-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.md-editor-modal {
|
||||
background: rgb(31, 41, 55);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.md-editor-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.md-editor-modal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-modal-close {
|
||||
padding: 0.5rem;
|
||||
color: rgb(156, 163, 175);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.md-editor-modal-close:hover {
|
||||
background: rgb(55, 65, 81);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-settings-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.md-editor-settings-group h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.md-editor-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(156, 163, 175);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-editor-checkbox:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-editor-settings-group select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
background: rgb(17, 24, 39);
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-editor-settings-group select:hover {
|
||||
border-color: rgb(75, 85, 99);
|
||||
}
|
||||
|
||||
.md-editor-settings-group select:focus {
|
||||
outline: none;
|
||||
border-color: rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
|
||||
/* Reset all backgrounds, margins, and padding */
|
||||
html,
|
||||
body {
|
||||
background: white !important;
|
||||
background-color: white !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide all elements by default */
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide UI elements completely - don't take up space */
|
||||
.md-editor-header,
|
||||
.md-editor-pane-header,
|
||||
.md-editor-textarea,
|
||||
.md-editor-modal-overlay,
|
||||
.md-editor-pane:first-child,
|
||||
header,
|
||||
nav,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset container backgrounds and layouts */
|
||||
.md-editor,
|
||||
.md-editor-wrapper,
|
||||
.md-editor-main,
|
||||
.md-editor-pane {
|
||||
background: white !important;
|
||||
background-color: white !important;
|
||||
border: none !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
.md-editor-main {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
/* Make preview and contents visible */
|
||||
.md-editor-preview,
|
||||
.md-editor-preview * {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.md-editor-preview {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.md-editor-preview h1,
|
||||
.md-editor-preview h2,
|
||||
.md-editor-preview h3,
|
||||
.md-editor-preview h4,
|
||||
.md-editor-preview h5,
|
||||
.md-editor-preview h6 {
|
||||
color: black !important;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.md-editor-preview p,
|
||||
.md-editor-preview li,
|
||||
.md-editor-preview span {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.md-editor-preview a {
|
||||
color: #0366d6 !important;
|
||||
}
|
||||
|
||||
.md-editor-preview code {
|
||||
background: #f6f8fa !important;
|
||||
color: #24292e !important;
|
||||
}
|
||||
|
||||
.md-editor-preview pre {
|
||||
background: #f6f8fa !important;
|
||||
border: 1px solid #e1e4e8 !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.md-editor-preview pre code {
|
||||
color: #24292e !important;
|
||||
}
|
||||
|
||||
.md-editor-preview blockquote {
|
||||
color: #6a737d !important;
|
||||
border-left-color: #dfe2e5 !important;
|
||||
}
|
||||
|
||||
.md-editor-preview table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.md-editor-preview th,
|
||||
.md-editor-preview td {
|
||||
border-color: #e1e4e8 !important;
|
||||
}
|
||||
|
||||
.md-editor-preview th {
|
||||
background: #f6f8fa !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.md-editor-preview tr:nth-child(even) {
|
||||
background: #f6f8fa !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.md-editor-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.md-editor-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.md-editor-btn {
|
||||
flex: 1 1 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.md-editor-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.md-editor-main {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.md-editor-pane {
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.md-editor-textarea,
|
||||
.md-editor-preview {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.md-editor-pane:first-child {
|
||||
border-right: none;
|
||||
border-bottom: 2px solid rgb(55, 65, 81);
|
||||
}
|
||||
|
||||
.md-editor.light-mode .md-editor-pane:first-child {
|
||||
border-right: none;
|
||||
border-bottom-color: rgb(229, 231, 235);
|
||||
}
|
||||
|
||||
.md-editor-modal {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.md-editor-settings-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) and (min-width: 769px) {
|
||||
.md-editor-main {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.md-editor-btn {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.md-editor-pane {
|
||||
min-height: 250px;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.md-editor-btn {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.md-editor-header {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.md-editor-actions {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
}
|
||||
@@ -6,68 +6,68 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-multi-tool.html',
|
||||
name: 'PDF Multi Tool',
|
||||
icon: 'pencil-ruler',
|
||||
icon: 'ph-pencil-ruler',
|
||||
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'merge-pdf.html',
|
||||
name: 'Merge PDF',
|
||||
icon: 'combine',
|
||||
icon: 'ph-browsers',
|
||||
subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'split-pdf.html',
|
||||
name: 'Split PDF',
|
||||
icon: 'scissors',
|
||||
icon: 'ph-scissors',
|
||||
subtitle: 'Extract a range of pages into a new PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'compress-pdf.html',
|
||||
name: 'Compress PDF',
|
||||
icon: 'zap',
|
||||
icon: 'ph-lightning',
|
||||
subtitle: 'Reduce the file size of your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'edit-pdf.html',
|
||||
name: 'PDF Editor',
|
||||
icon: 'pocket-knife',
|
||||
icon: 'ph-pencil-simple',
|
||||
subtitle:
|
||||
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'jpg-to-pdf.html',
|
||||
name: 'JPG to PDF',
|
||||
icon: 'image-up',
|
||||
icon: 'ph-file-jpg',
|
||||
subtitle: 'Create a PDF from one or more JPG images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'sign-pdf.html',
|
||||
name: 'Sign PDF',
|
||||
icon: 'pen-tool',
|
||||
icon: 'ph-pen-nib',
|
||||
subtitle: 'Draw, type, or upload your signature.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'crop-pdf.html',
|
||||
name: 'Crop PDF',
|
||||
icon: 'crop',
|
||||
icon: 'ph-crop',
|
||||
subtitle: 'Trim the margins of every page in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'extract-pages.html',
|
||||
name: 'Extract Pages',
|
||||
icon: 'ungroup',
|
||||
icon: 'ph-squares-four',
|
||||
subtitle: 'Save a selection of pages as new files.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'organize-pdf.html',
|
||||
name: 'Duplicate & Organize',
|
||||
icon: 'files',
|
||||
icon: 'ph-files',
|
||||
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'delete-pages.html',
|
||||
name: 'Delete Pages',
|
||||
icon: 'trash-2',
|
||||
icon: 'ph-trash',
|
||||
subtitle: 'Remove specific pages from your document.',
|
||||
},
|
||||
],
|
||||
@@ -78,98 +78,98 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'edit-pdf.html',
|
||||
name: 'PDF Editor',
|
||||
icon: 'pocket-knife',
|
||||
icon: 'ph-pencil-simple',
|
||||
subtitle:
|
||||
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'bookmark.html',
|
||||
name: 'Edit Bookmarks',
|
||||
icon: 'bookmark',
|
||||
icon: 'ph-bookmark',
|
||||
subtitle: 'Add, edit, import, delete and extract PDF bookmarks.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'table-of-contents.html',
|
||||
name: 'Table of Contents',
|
||||
icon: 'list',
|
||||
icon: 'ph-list',
|
||||
subtitle: 'Generate a table of contents page from PDF bookmarks.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'page-numbers.html',
|
||||
name: 'Page Numbers',
|
||||
icon: 'list-ordered',
|
||||
icon: 'ph-list-numbers',
|
||||
subtitle: 'Insert page numbers into your document.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'add-watermark.html',
|
||||
name: 'Add Watermark',
|
||||
icon: 'droplets',
|
||||
icon: 'ph-drop',
|
||||
subtitle: 'Stamp text or an image over your PDF pages.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'header-footer.html',
|
||||
name: 'Header & Footer',
|
||||
icon: 'pilcrow',
|
||||
icon: 'ph-paragraph',
|
||||
subtitle: 'Add text to the top and bottom of pages.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'invert-colors.html',
|
||||
name: 'Invert Colors',
|
||||
icon: 'contrast',
|
||||
icon: 'ph-circle-half',
|
||||
subtitle: 'Create a "dark mode" version of your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'background-color.html',
|
||||
name: 'Background Color',
|
||||
icon: 'palette',
|
||||
icon: 'ph-palette',
|
||||
subtitle: 'Change the background color of your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'text-color.html',
|
||||
name: 'Change Text Color',
|
||||
icon: 'type',
|
||||
icon: 'ph-eyedropper',
|
||||
subtitle: 'Change the color of text in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'sign-pdf.html',
|
||||
name: 'Sign PDF',
|
||||
icon: 'pen-tool',
|
||||
icon: 'ph-pen-nib',
|
||||
subtitle: 'Draw, type, or upload your signature.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'add-stamps.html',
|
||||
name: 'Add Stamps',
|
||||
icon: 'stamp',
|
||||
icon: 'ph-stamp',
|
||||
subtitle: 'Add image stamps to your PDF using the annotation toolbar.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'remove-annotations.html',
|
||||
name: 'Remove Annotations',
|
||||
icon: 'eraser',
|
||||
icon: 'ph-eraser',
|
||||
subtitle: 'Strip comments, highlights, and links.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'crop-pdf.html',
|
||||
name: 'Crop PDF',
|
||||
icon: 'crop',
|
||||
icon: 'ph-crop',
|
||||
subtitle: 'Trim the margins of every page in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'form-filler.html',
|
||||
name: 'PDF Form Filler',
|
||||
icon: 'square-pen',
|
||||
icon: 'ph-pencil-line',
|
||||
subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'form-creator.html',
|
||||
name: 'Create PDF Form',
|
||||
icon: 'file-input',
|
||||
icon: 'ph-file-plus',
|
||||
subtitle: 'Create fillable PDF forms with drag-and-drop text fields.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'remove-blank-pages.html',
|
||||
name: 'Remove Blank Pages',
|
||||
icon: 'file-minus-2',
|
||||
icon: 'ph-file-minus',
|
||||
subtitle: 'Automatically detect and delete blank pages.',
|
||||
},
|
||||
],
|
||||
@@ -179,64 +179,196 @@ export const categories = [
|
||||
tools: [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'image-to-pdf.html',
|
||||
name: 'Image to PDF',
|
||||
icon: 'images',
|
||||
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.',
|
||||
name: 'Images to PDF',
|
||||
icon: 'ph-images',
|
||||
subtitle: 'Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'jpg-to-pdf.html',
|
||||
name: 'JPG to PDF',
|
||||
icon: 'image-up',
|
||||
icon: 'ph-file-jpg',
|
||||
subtitle: 'Create a PDF from one or more JPG images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'png-to-pdf.html',
|
||||
name: 'PNG to PDF',
|
||||
icon: 'image-up',
|
||||
icon: 'ph-file-png',
|
||||
subtitle: 'Create a PDF from one or more PNG images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'webp-to-pdf.html',
|
||||
name: 'WebP to PDF',
|
||||
icon: 'image-up',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Create a PDF from one or more WebP images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'svg-to-pdf.html',
|
||||
name: 'SVG to PDF',
|
||||
icon: 'pen-tool',
|
||||
icon: 'ph-file-svg',
|
||||
subtitle: 'Create a PDF from one or more SVG images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'bmp-to-pdf.html',
|
||||
name: 'BMP to PDF',
|
||||
icon: 'image',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Create a PDF from one or more BMP images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'heic-to-pdf.html',
|
||||
name: 'HEIC to PDF',
|
||||
icon: 'smartphone',
|
||||
icon: 'ph-device-mobile',
|
||||
subtitle: 'Create a PDF from one or more HEIC images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'tiff-to-pdf.html',
|
||||
name: 'TIFF to PDF',
|
||||
icon: 'layers',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Create a PDF from one or more TIFF images.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'txt-to-pdf.html',
|
||||
name: 'Text to PDF',
|
||||
icon: 'file-pen',
|
||||
icon: 'ph-text-t',
|
||||
subtitle: 'Convert a plain text file into a PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'markdown-to-pdf.html',
|
||||
name: 'Markdown to PDF',
|
||||
icon: 'ph-markdown-logo',
|
||||
subtitle: 'Convert Markdown to PDF with live preview and syntax highlighting.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'json-to-pdf.html',
|
||||
name: 'JSON to PDF',
|
||||
icon: 'file-code',
|
||||
icon: 'ph-file-code',
|
||||
subtitle: 'Convert JSON files to PDF format.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'odt-to-pdf.html',
|
||||
name: 'ODT to PDF',
|
||||
icon: 'ph-file',
|
||||
subtitle: 'Convert ODT (OpenDocument Text) files to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'csv-to-pdf.html',
|
||||
name: 'CSV to PDF',
|
||||
icon: 'ph-file-csv',
|
||||
subtitle: 'Convert CSV (Comma-Separated Values) spreadsheets to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'rtf-to-pdf.html',
|
||||
name: 'RTF to PDF',
|
||||
icon: 'ph-file-text',
|
||||
subtitle: 'Convert RTF (Rich Text Format) documents to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'word-to-pdf.html',
|
||||
name: 'Word to PDF',
|
||||
icon: 'ph-microsoft-word-logo',
|
||||
subtitle: 'Convert Word documents (DOCX, DOC, ODT) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'excel-to-pdf.html',
|
||||
name: 'Excel to PDF',
|
||||
icon: 'ph-microsoft-excel-logo',
|
||||
subtitle: 'Convert Excel spreadsheets (XLSX, XLS, ODS) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'powerpoint-to-pdf.html',
|
||||
name: 'PowerPoint to PDF',
|
||||
icon: 'ph-microsoft-powerpoint-logo',
|
||||
subtitle: 'Convert PowerPoint presentations (PPTX, PPT, ODP) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'xps-to-pdf.html',
|
||||
name: 'XPS to PDF',
|
||||
icon: 'ph-scan',
|
||||
subtitle: 'Convert XPS/OXPS documents to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'mobi-to-pdf.html',
|
||||
name: 'MOBI to PDF',
|
||||
icon: 'ph-book-open-text',
|
||||
subtitle: 'Convert MOBI e-books to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'epub-to-pdf.html',
|
||||
name: 'EPUB to PDF',
|
||||
icon: 'ph-book-open-text',
|
||||
subtitle: 'Convert EPUB e-books to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'fb2-to-pdf.html',
|
||||
name: 'FB2 to PDF',
|
||||
icon: 'ph-book-bookmark',
|
||||
subtitle: 'Convert FictionBook (FB2) e-books to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'cbz-to-pdf.html',
|
||||
name: 'CBZ to PDF',
|
||||
icon: 'ph-book-open',
|
||||
subtitle: 'Convert comic book archives (CBZ/CBR) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'wpd-to-pdf.html',
|
||||
name: 'WPD to PDF',
|
||||
icon: 'ph-file-text',
|
||||
subtitle: 'Convert WordPerfect documents (WPD) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'wps-to-pdf.html',
|
||||
name: 'WPS to PDF',
|
||||
icon: 'ph-file-text',
|
||||
subtitle: 'Convert WPS Office documents to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'xml-to-pdf.html',
|
||||
name: 'XML to PDF',
|
||||
icon: 'ph-file-code',
|
||||
subtitle: 'Convert XML documents to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pages-to-pdf.html',
|
||||
name: 'Pages to PDF',
|
||||
icon: 'ph-file-text',
|
||||
subtitle: 'Convert Apple Pages documents to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'odg-to-pdf.html',
|
||||
name: 'ODG to PDF',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Convert OpenDocument Graphics (ODG) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'ods-to-pdf.html',
|
||||
name: 'ODS to PDF',
|
||||
icon: 'ph-table',
|
||||
subtitle: 'Convert OpenDocument Spreadsheet (ODS) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'odp-to-pdf.html',
|
||||
name: 'ODP to PDF',
|
||||
icon: 'ph-presentation',
|
||||
subtitle: 'Convert OpenDocument Presentation (ODP) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pub-to-pdf.html',
|
||||
name: 'PUB to PDF',
|
||||
icon: 'ph-book-open',
|
||||
subtitle: 'Convert Microsoft Publisher (PUB) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'vsd-to-pdf.html',
|
||||
name: 'VSD to PDF',
|
||||
icon: 'ph-git-branch',
|
||||
subtitle: 'Convert Microsoft Visio (VSD, VSDX) to PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'psd-to-pdf.html',
|
||||
name: 'PSD to PDF',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Convert Adobe Photoshop (PSD) files to PDF. Multiple files supported.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -245,45 +377,93 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-jpg.html',
|
||||
name: 'PDF to JPG',
|
||||
icon: 'file-image',
|
||||
icon: 'ph-file-image',
|
||||
subtitle: 'Convert each PDF page into a JPG image.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-png.html',
|
||||
name: 'PDF to PNG',
|
||||
icon: 'file-image',
|
||||
icon: 'ph-file-image',
|
||||
subtitle: 'Convert each PDF page into a PNG image.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-webp.html',
|
||||
name: 'PDF to WebP',
|
||||
icon: 'file-image',
|
||||
icon: 'ph-file-image',
|
||||
subtitle: 'Convert each PDF page into a WebP image.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-bmp.html',
|
||||
name: 'PDF to BMP',
|
||||
icon: 'file-image',
|
||||
icon: 'ph-file-image',
|
||||
subtitle: 'Convert each PDF page into a BMP image.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-tiff.html',
|
||||
name: 'PDF to TIFF',
|
||||
icon: 'file-image',
|
||||
icon: 'ph-file-image',
|
||||
subtitle: 'Convert each PDF page into a TIFF image.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-svg.html',
|
||||
name: 'PDF to SVG',
|
||||
icon: 'ph-file-code',
|
||||
subtitle: 'Convert each PDF page into a scalable vector graphic.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-csv.html',
|
||||
name: 'PDF to CSV',
|
||||
icon: 'ph-file-csv',
|
||||
subtitle: 'Extract tables from PDF and convert to CSV format.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-excel.html',
|
||||
name: 'PDF to Excel',
|
||||
icon: 'ph-microsoft-excel-logo',
|
||||
subtitle: 'Extract tables from PDF and convert to Excel (XLSX).',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-greyscale.html',
|
||||
name: 'PDF to Greyscale',
|
||||
icon: 'palette',
|
||||
icon: 'ph-palette',
|
||||
subtitle: 'Convert all colors to black and white.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-json.html',
|
||||
name: 'PDF to JSON',
|
||||
icon: 'file-code',
|
||||
icon: 'ph-file-code',
|
||||
subtitle: 'Convert PDF files to JSON format.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-docx.html',
|
||||
name: 'PDF to Word',
|
||||
icon: 'ph-microsoft-word-logo',
|
||||
subtitle: 'Convert PDF files to editable Word documents.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'extract-images.html',
|
||||
name: 'Extract Images',
|
||||
icon: 'ph-download-simple',
|
||||
subtitle: 'Extract all embedded images from your PDF files.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-markdown.html',
|
||||
name: 'PDF to Markdown',
|
||||
icon: 'ph-markdown-logo',
|
||||
subtitle: 'Convert PDF text and tables to Markdown format.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'prepare-pdf-for-ai.html',
|
||||
name: 'Prepare PDF for AI',
|
||||
icon: 'ph-sparkle',
|
||||
subtitle: 'Extract PDF content as LlamaIndex JSON for RAG/LLM pipelines.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-text.html',
|
||||
name: 'PDF to Text',
|
||||
icon: 'ph-text-aa',
|
||||
subtitle: 'Extract text from PDF files and save as plain text (.txt).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -292,133 +472,157 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'ocr-pdf.html',
|
||||
name: 'OCR PDF',
|
||||
icon: 'scan-text',
|
||||
icon: 'ph-barcode',
|
||||
subtitle: 'Make a PDF searchable and copyable.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'merge-pdf.html',
|
||||
name: 'Merge PDF',
|
||||
icon: 'combine',
|
||||
icon: 'ph-browsers',
|
||||
subtitle: 'Combine multiple PDFs into one file.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'alternate-merge.html',
|
||||
name: 'Alternate & Mix Pages',
|
||||
icon: 'shuffle',
|
||||
icon: 'ph-shuffle',
|
||||
subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'organize-pdf.html',
|
||||
name: 'Organize & Duplicate',
|
||||
icon: 'files',
|
||||
icon: 'ph-files',
|
||||
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'add-attachments.html',
|
||||
name: 'Add Attachments',
|
||||
icon: 'paperclip',
|
||||
icon: 'ph-paperclip',
|
||||
subtitle: 'Embed one or more files into your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'extract-attachments.html',
|
||||
name: 'Extract Attachments',
|
||||
icon: 'download',
|
||||
icon: 'ph-download',
|
||||
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'edit-attachments.html',
|
||||
name: 'Edit Attachments',
|
||||
icon: 'file-edit',
|
||||
icon: 'ph-paperclip-horizontal',
|
||||
subtitle: 'View or remove attachments in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-multi-tool.html',
|
||||
name: 'PDF Multi Tool',
|
||||
icon: 'pencil-ruler',
|
||||
icon: 'ph-pencil-ruler',
|
||||
subtitle: 'Full-featured PDF editor with page management.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-layers.html',
|
||||
name: 'PDF OCG',
|
||||
icon: 'ph-stack-simple',
|
||||
subtitle: 'View, toggle, add, and delete OCG layers in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'extract-tables.html',
|
||||
name: 'Extract Tables',
|
||||
icon: 'ph-table',
|
||||
subtitle: 'Extract tables from PDFs as CSV, JSON, or Markdown.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'split-pdf.html',
|
||||
name: 'Split PDF',
|
||||
icon: 'scissors',
|
||||
icon: 'ph-scissors',
|
||||
subtitle: 'Extract a range of pages into a new PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'divide-pages.html',
|
||||
name: 'Divide Pages',
|
||||
icon: 'table-columns-split',
|
||||
icon: 'ph-columns',
|
||||
subtitle: 'Divide pages horizontally or vertically.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'extract-pages.html',
|
||||
name: 'Extract Pages',
|
||||
icon: 'ungroup',
|
||||
icon: 'ph-squares-four',
|
||||
subtitle: 'Save a selection of pages as new files.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'delete-pages.html',
|
||||
name: 'Delete Pages',
|
||||
icon: 'trash-2',
|
||||
icon: 'ph-trash',
|
||||
subtitle: 'Remove specific pages from your document.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'add-blank-page.html',
|
||||
name: 'Add Blank Page',
|
||||
icon: 'file-plus-2',
|
||||
icon: 'ph-file-plus',
|
||||
subtitle: 'Insert an empty page anywhere in your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'reverse-pages.html',
|
||||
name: 'Reverse Pages',
|
||||
icon: 'arrow-down-z-a',
|
||||
icon: 'ph-sort-descending',
|
||||
subtitle: 'Flip the order of all pages in your document.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'rotate-pdf.html',
|
||||
name: 'Rotate PDF',
|
||||
icon: 'rotate-cw',
|
||||
icon: 'ph-arrow-clockwise',
|
||||
subtitle: 'Turn pages in 90-degree increments.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'rotate-custom.html',
|
||||
name: 'Rotate by Custom Degrees',
|
||||
icon: 'ph-arrows-clockwise',
|
||||
subtitle: 'Rotate pages by any custom angle.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'n-up-pdf.html',
|
||||
name: 'N-Up PDF',
|
||||
icon: 'layout-grid',
|
||||
icon: 'ph-squares-four',
|
||||
subtitle: 'Arrange multiple pages onto a single sheet.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-booklet.html',
|
||||
name: 'PDF Booklet',
|
||||
icon: 'ph-book-open',
|
||||
subtitle: 'Rearrange pages for double-sided booklet printing.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'combine-single-page.html',
|
||||
name: 'Combine to Single Page',
|
||||
icon: 'unfold-vertical',
|
||||
icon: 'ph-arrows-out-line-vertical',
|
||||
subtitle: 'Stitch all pages into one continuous scroll.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'view-metadata.html',
|
||||
name: 'View Metadata',
|
||||
icon: 'info',
|
||||
icon: 'ph-info',
|
||||
subtitle: 'Inspect the hidden properties of your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'edit-metadata.html',
|
||||
name: 'Edit Metadata',
|
||||
icon: 'file-cog',
|
||||
icon: 'ph-file-code',
|
||||
subtitle: 'Change the author, title, and other properties.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-zip.html',
|
||||
name: 'PDFs to ZIP',
|
||||
icon: 'stretch-horizontal',
|
||||
icon: 'ph-file-zip',
|
||||
subtitle: 'Package multiple PDF files into a ZIP archive.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'compare-pdfs.html',
|
||||
name: 'Compare PDFs',
|
||||
icon: 'git-compare',
|
||||
icon: 'ph-git-diff',
|
||||
subtitle: 'Compare two PDFs side by side.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'posterize-pdf.html',
|
||||
name: 'Posterize PDF',
|
||||
icon: 'notepad-text-dashed',
|
||||
icon: 'ph-notepad',
|
||||
subtitle: 'Split a large page into multiple smaller pages.',
|
||||
},
|
||||
],
|
||||
@@ -429,40 +633,52 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'compress-pdf.html',
|
||||
name: 'Compress PDF',
|
||||
icon: 'zap',
|
||||
icon: 'ph-lightning',
|
||||
subtitle: 'Reduce the file size of your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'pdf-to-pdfa.html',
|
||||
name: 'PDF to PDF/A',
|
||||
icon: 'ph-archive',
|
||||
subtitle: 'Convert PDF to PDF/A for long-term archiving.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'fix-page-size.html',
|
||||
name: 'Fix Page Size',
|
||||
icon: 'ruler-dimension-line',
|
||||
icon: 'ph-ruler',
|
||||
subtitle: 'Standardize all pages to a uniform size.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'linearize-pdf.html',
|
||||
name: 'Linearize PDF',
|
||||
icon: 'gauge',
|
||||
icon: 'ph-gauge',
|
||||
subtitle: 'Optimize PDF for fast web viewing.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'page-dimensions.html',
|
||||
name: 'Page Dimensions',
|
||||
icon: 'ruler',
|
||||
icon: 'ph-ruler',
|
||||
subtitle: 'Analyze page size, orientation, and units.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'remove-restrictions.html',
|
||||
name: 'Remove Restrictions',
|
||||
icon: 'unlink',
|
||||
icon: 'ph-link-break',
|
||||
subtitle:
|
||||
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'repair-pdf.html',
|
||||
name: 'Repair PDF',
|
||||
icon: 'wrench',
|
||||
icon: 'ph-wrench',
|
||||
subtitle: 'Recover data from corrupted or damaged PDF files.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'rasterize-pdf.html',
|
||||
name: 'Rasterize PDF',
|
||||
icon: 'ph-image',
|
||||
subtitle: 'Convert PDF to image-based PDF. Flatten layers and remove selectable text.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -471,37 +687,37 @@ export const categories = [
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'encrypt-pdf.html',
|
||||
name: 'Encrypt PDF',
|
||||
icon: 'lock',
|
||||
icon: 'ph-lock',
|
||||
subtitle: 'Lock your PDF by adding a password.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'sanitize-pdf.html',
|
||||
name: 'Sanitize PDF',
|
||||
icon: 'brush-cleaning',
|
||||
icon: 'ph-broom',
|
||||
subtitle: 'Remove metadata, annotations, scripts, and more.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'decrypt-pdf.html',
|
||||
name: 'Decrypt PDF',
|
||||
icon: 'unlock',
|
||||
icon: 'ph-lock-open',
|
||||
subtitle: 'Unlock PDF by removing password protection.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'flatten-pdf.html',
|
||||
name: 'Flatten PDF',
|
||||
icon: 'layers',
|
||||
icon: 'ph-stack',
|
||||
subtitle: 'Make form fields and annotations non-editable.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'remove-metadata.html',
|
||||
name: 'Remove Metadata',
|
||||
icon: 'file-x',
|
||||
icon: 'ph-file-x',
|
||||
subtitle: 'Strip hidden data from your PDF.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'change-permissions.html',
|
||||
name: 'Change Permissions',
|
||||
icon: 'shield-check',
|
||||
icon: 'ph-shield-check',
|
||||
subtitle: 'Set or change user permissions on a PDF.',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Simple FAQ accordion handler for standalone pages
|
||||
window.addEventListener('load', () => {
|
||||
const faqAccordion = document.getElementById('faq-accordion');
|
||||
if (faqAccordion) {
|
||||
faqAccordion.addEventListener('click', (e) => {
|
||||
const questionButton = (e.target as HTMLElement).closest('.faq-question');
|
||||
if (!questionButton) return;
|
||||
|
||||
const faqItem = questionButton.parentElement;
|
||||
const answer = faqItem?.querySelector('.faq-answer') as HTMLElement;
|
||||
|
||||
if (!faqItem || !answer) return;
|
||||
|
||||
faqItem.classList.toggle('open');
|
||||
|
||||
if (faqItem.classList.contains('open')) {
|
||||
answer.style.maxHeight = answer.scrollHeight + 'px';
|
||||
} else {
|
||||
answer.style.maxHeight = '0px';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -44,7 +44,7 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
||||
ns: ['common', 'tools'],
|
||||
defaultNS: 'common',
|
||||
backend: {
|
||||
loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}/{{ns}}.json`,
|
||||
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
|
||||
},
|
||||
detection: {
|
||||
order: ['path', 'localStorage', 'navigator'],
|
||||
|
||||
201
src/js/logic/cbz-to-pdf-page.ts
Normal file
201
src/js/logic/cbz-to-pdf-page.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const FILETYPE = 'cbz';
|
||||
const EXTENSIONS = ['.cbz', '.cbr'];
|
||||
const TOOL_NAME = 'CBZ';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
@@ -7,187 +7,94 @@ import {
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
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);
|
||||
const CONDENSE_PRESETS = {
|
||||
light: {
|
||||
images: { quality: 90, dpiTarget: 150, dpiThreshold: 200 },
|
||||
scrub: { metadata: false, thumbnails: true },
|
||||
subsetFonts: true,
|
||||
},
|
||||
balanced: {
|
||||
images: { quality: 75, dpiTarget: 96, dpiThreshold: 150 },
|
||||
scrub: { metadata: true, thumbnails: true },
|
||||
subsetFonts: true,
|
||||
},
|
||||
aggressive: {
|
||||
images: { quality: 50, dpiTarget: 72, dpiThreshold: 100 },
|
||||
scrub: { metadata: true, thumbnails: true, xmlMetadata: true },
|
||||
subsetFonts: true,
|
||||
},
|
||||
extreme: {
|
||||
images: { quality: 30, dpiTarget: 60, dpiThreshold: 96 },
|
||||
scrub: { metadata: true, thumbnails: true, xmlMetadata: true },
|
||||
subsetFonts: true,
|
||||
},
|
||||
};
|
||||
|
||||
const PHOTON_PRESETS = {
|
||||
light: { scale: 2.0, quality: 0.85 },
|
||||
balanced: { scale: 1.5, quality: 0.65 },
|
||||
aggressive: { scale: 1.2, quality: 0.45 },
|
||||
extreme: { scale: 1.0, quality: 0.25 },
|
||||
};
|
||||
|
||||
async function performCondenseCompression(
|
||||
fileBlob: Blob,
|
||||
level: string,
|
||||
customSettings?: {
|
||||
imageQuality?: number;
|
||||
dpiTarget?: number;
|
||||
dpiThreshold?: number;
|
||||
removeMetadata?: boolean;
|
||||
subsetFonts?: boolean;
|
||||
convertToGrayscale?: boolean;
|
||||
removeThumbnails?: boolean;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
) {
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const pages = pdfDoc.getPages();
|
||||
const preset = CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] || CONDENSE_PRESETS.balanced;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
const dpiTarget = customSettings?.dpiTarget ?? preset.images.dpiTarget;
|
||||
const userThreshold = customSettings?.dpiThreshold ?? preset.images.dpiThreshold;
|
||||
const dpiThreshold = Math.max(userThreshold, dpiTarget + 10);
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveOptions = {
|
||||
useObjectStreams: settings.useObjectStreams !== false,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: settings.objectsPerTick || 50,
|
||||
const options = {
|
||||
images: {
|
||||
enabled: true,
|
||||
quality: customSettings?.imageQuality ?? preset.images.quality,
|
||||
dpiTarget,
|
||||
dpiThreshold,
|
||||
convertToGray: customSettings?.convertToGrayscale ?? false,
|
||||
},
|
||||
scrub: {
|
||||
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
|
||||
thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails,
|
||||
xmlMetadata: (preset.scrub as any).xmlMetadata ?? false,
|
||||
},
|
||||
subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts,
|
||||
save: {
|
||||
garbage: 4 as const,
|
||||
deflate: true,
|
||||
clean: true,
|
||||
useObjstms: true,
|
||||
},
|
||||
};
|
||||
|
||||
return await pdfDoc.save(saveOptions);
|
||||
const result = await pymupdf.compressPdf(fileBlob, options);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||
async function performPhotonCompression(arrayBuffer: ArrayBuffer, level: string) {
|
||||
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
const settings = PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || PHOTON_PRESETS.balanced;
|
||||
|
||||
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
||||
const page = await pdfJsDoc.getPage(i);
|
||||
@@ -197,13 +104,12 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||
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)
|
||||
const jpegBlob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((blob) => resolve(blob as Blob), 'image/jpeg', settings.quality)
|
||||
);
|
||||
const jpegBytes = await (jpegBlob as Blob).arrayBuffer();
|
||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(jpegImage, {
|
||||
@@ -219,13 +125,19 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const compressOptions = document.getElementById('compress-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const algorithmSelect = document.getElementById('compression-algorithm') as HTMLSelectElement;
|
||||
const condenseInfo = document.getElementById('condense-info');
|
||||
const photonInfo = document.getElementById('photon-info');
|
||||
const toggleCustomSettings = document.getElementById('toggle-custom-settings');
|
||||
const customSettingsPanel = document.getElementById('custom-settings-panel');
|
||||
const customSettingsChevron = document.getElementById('custom-settings-chevron');
|
||||
|
||||
let useCustomSettings = false;
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
@@ -233,60 +145,79 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle algorithm info
|
||||
if (algorithmSelect && condenseInfo && photonInfo) {
|
||||
algorithmSelect.addEventListener('change', () => {
|
||||
if (algorithmSelect.value === 'condense') {
|
||||
condenseInfo.classList.remove('hidden');
|
||||
photonInfo.classList.add('hidden');
|
||||
} else {
|
||||
condenseInfo.classList.add('hidden');
|
||||
photonInfo.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle custom settings panel
|
||||
if (toggleCustomSettings && customSettingsPanel && customSettingsChevron) {
|
||||
toggleCustomSettings.addEventListener('click', () => {
|
||||
customSettingsPanel.classList.toggle('hidden');
|
||||
customSettingsChevron.style.transform = customSettingsPanel.classList.contains('hidden')
|
||||
? 'rotate(0deg)'
|
||||
: 'rotate(180deg)';
|
||||
// Mark that user wants to use custom settings
|
||||
if (!customSettingsPanel.classList.contains('hidden')) {
|
||||
useCustomSettings = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !compressOptions || !processBtn || !fileControls) return;
|
||||
if (!compressOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
}
|
||||
compressOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
compressOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
// Clear file display area
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -297,8 +228,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const compressionLevel = document.getElementById('compression-level') as HTMLSelectElement;
|
||||
if (compressionLevel) compressionLevel.value = 'balanced';
|
||||
|
||||
const compressionAlgorithm = document.getElementById('compression-algorithm') as HTMLSelectElement;
|
||||
if (compressionAlgorithm) compressionAlgorithm.value = 'vector';
|
||||
if (algorithmSelect) algorithmSelect.value = 'condense';
|
||||
|
||||
useCustomSettings = false;
|
||||
if (customSettingsPanel) customSettingsPanel.classList.add('hidden');
|
||||
if (customSettingsChevron) customSettingsChevron.style.transform = 'rotate(0deg)';
|
||||
|
||||
const imageQuality = document.getElementById('image-quality') as HTMLInputElement;
|
||||
const dpiTarget = document.getElementById('dpi-target') as HTMLInputElement;
|
||||
const dpiThreshold = document.getElementById('dpi-threshold') as HTMLInputElement;
|
||||
const removeMetadata = document.getElementById('remove-metadata') as HTMLInputElement;
|
||||
const subsetFonts = document.getElementById('subset-fonts') as HTMLInputElement;
|
||||
const convertToGrayscale = document.getElementById('convert-to-grayscale') as HTMLInputElement;
|
||||
const removeThumbnails = document.getElementById('remove-thumbnails') as HTMLInputElement;
|
||||
|
||||
if (imageQuality) imageQuality.value = '75';
|
||||
if (dpiTarget) dpiTarget.value = '96';
|
||||
if (dpiThreshold) dpiThreshold.value = '150';
|
||||
if (removeMetadata) removeMetadata.checked = true;
|
||||
if (subsetFonts) subsetFonts.checked = true;
|
||||
if (convertToGrayscale) convertToGrayscale.checked = false;
|
||||
if (removeThumbnails) removeThumbnails.checked = true;
|
||||
|
||||
if (condenseInfo) condenseInfo.classList.remove('hidden');
|
||||
if (photonInfo) photonInfo.classList.add('hidden');
|
||||
|
||||
updateUI();
|
||||
};
|
||||
@@ -306,52 +259,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const compress = async () => {
|
||||
const level = (document.getElementById('compression-level') as HTMLSelectElement).value;
|
||||
const algorithm = (document.getElementById('compression-algorithm') as HTMLSelectElement).value;
|
||||
const convertToGrayscale = (document.getElementById('convert-to-grayscale') as HTMLInputElement)?.checked ?? false;
|
||||
|
||||
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 },
|
||||
},
|
||||
};
|
||||
let customSettings: {
|
||||
imageQuality?: number;
|
||||
dpiTarget?: number;
|
||||
dpiThreshold?: number;
|
||||
removeMetadata?: boolean;
|
||||
subsetFonts?: boolean;
|
||||
convertToGrayscale?: boolean;
|
||||
removeThumbnails?: boolean;
|
||||
} | undefined;
|
||||
|
||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||
const legacySettings = settings[level].legacy;
|
||||
if (useCustomSettings) {
|
||||
const imageQuality = parseInt((document.getElementById('image-quality') as HTMLInputElement)?.value) || 75;
|
||||
const dpiTarget = parseInt((document.getElementById('dpi-target') as HTMLInputElement)?.value) || 96;
|
||||
const dpiThreshold = parseInt((document.getElementById('dpi-threshold') as HTMLInputElement)?.value) || 150;
|
||||
const removeMetadata = (document.getElementById('remove-metadata') as HTMLInputElement)?.checked ?? true;
|
||||
const subsetFonts = (document.getElementById('subset-fonts') as HTMLInputElement)?.checked ?? true;
|
||||
const removeThumbnails = (document.getElementById('remove-thumbnails') as HTMLInputElement)?.checked ?? true;
|
||||
|
||||
customSettings = {
|
||||
imageQuality,
|
||||
dpiTarget,
|
||||
dpiThreshold,
|
||||
removeMetadata,
|
||||
subsetFonts,
|
||||
convertToGrayscale,
|
||||
removeThumbnails,
|
||||
};
|
||||
} else {
|
||||
customSettings = convertToGrayscale ? { convertToGrayscale } : undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
@@ -362,49 +301,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
||||
|
||||
let resultBytes;
|
||||
let usedMethod;
|
||||
let resultBlob: Blob;
|
||||
let resultSize: number;
|
||||
let usedMethod: string;
|
||||
|
||||
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';
|
||||
if (algorithm === 'condense') {
|
||||
showLoader('Loading engine...');
|
||||
const result = await performCondenseCompression(originalFile, level, customSettings);
|
||||
resultBlob = result.blob;
|
||||
resultSize = result.compressedSize;
|
||||
usedMethod = 'Condense';
|
||||
} else {
|
||||
showLoader('Running Automatic (Vector first)...');
|
||||
const vectorResultBytes = await performSmartCompression(
|
||||
arrayBuffer,
|
||||
smartSettings
|
||||
);
|
||||
|
||||
if (vectorResultBytes.length < originalFile.size) {
|
||||
resultBytes = vectorResultBytes;
|
||||
usedMethod = 'Vector (Automatic)';
|
||||
} else {
|
||||
showAlert('Vector failed to reduce size. Trying Photon...', 'info');
|
||||
showLoader('Running Automatic (Photon fallback)...');
|
||||
resultBytes = await performLegacyCompression(
|
||||
arrayBuffer,
|
||||
legacySettings
|
||||
);
|
||||
usedMethod = 'Photon (Automatic)';
|
||||
}
|
||||
showLoader('Running Photon compression...');
|
||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile) as ArrayBuffer;
|
||||
const resultBytes = await performPhotonCompression(arrayBuffer, level);
|
||||
const buffer = resultBytes.buffer.slice(resultBytes.byteOffset, resultBytes.byteOffset + resultBytes.byteLength) as ArrayBuffer;
|
||||
resultBlob = new Blob([buffer], { type: 'application/pdf' });
|
||||
resultSize = resultBytes.length;
|
||||
usedMethod = 'Photon';
|
||||
}
|
||||
|
||||
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;
|
||||
const compressedSize = formatBytes(resultSize);
|
||||
const savings = originalFile.size - resultSize;
|
||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||
|
||||
downloadFile(
|
||||
new Blob([resultBytes], { type: 'application/pdf' }),
|
||||
'compressed-final.pdf'
|
||||
resultBlob,
|
||||
originalFile.name.replace(/\.pdf$/i, '') + '_compressed.pdf'
|
||||
);
|
||||
|
||||
hideLoader();
|
||||
@@ -419,7 +344,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
showAlert(
|
||||
'Compression Finished',
|
||||
`Method: ${usedMethod}. Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||
`Method: ${usedMethod}. Could not reduce file size further. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||
'warning',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -434,22 +359,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
totalOriginalSize += file.size;
|
||||
|
||||
let resultBytes;
|
||||
if (algorithm === 'vector') {
|
||||
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
} else if (algorithm === 'photon') {
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
let resultBytes: Uint8Array;
|
||||
if (algorithm === 'condense') {
|
||||
const result = await performCondenseCompression(file, level, customSettings);
|
||||
resultBytes = new Uint8Array(await result.blob.arrayBuffer());
|
||||
} else {
|
||||
const vectorResultBytes = await performSmartCompression(
|
||||
arrayBuffer,
|
||||
smartSettings
|
||||
);
|
||||
resultBytes = vectorResultBytes.length < file.size
|
||||
? vectorResultBytes
|
||||
: await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
|
||||
resultBytes = await performPhotonCompression(arrayBuffer, level);
|
||||
}
|
||||
|
||||
totalCompressedSize += resultBytes.length;
|
||||
@@ -459,10 +377,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
const totalSavings = totalOriginalSize - totalCompressedSize;
|
||||
const totalSavingsPercent =
|
||||
totalSavings > 0
|
||||
? ((totalSavings / totalOriginalSize) * 100).toFixed(1)
|
||||
: 0;
|
||||
const totalSavingsPercent = totalSavings > 0
|
||||
? ((totalSavings / totalOriginalSize) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
downloadFile(zipBlob, 'compressed-pdfs.zip');
|
||||
|
||||
@@ -486,6 +403,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
console.error('[CompressPDF] Error:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during compression. Error: ${e.message}`
|
||||
@@ -520,7 +438,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf');
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
pdfFiles.forEach(f => dataTransfer.items.add(f));
|
||||
@@ -529,7 +447,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
231
src/js/logic/csv-to-pdf-page.ts
Normal file
231
src/js/logic/csv-to-pdf-page.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
console.log('[CSV2PDF] Starting conversion...');
|
||||
console.log('[CSV2PDF] Number of files:', state.files.length);
|
||||
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one CSV file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const { convertCsvToPdf } = await import('../utils/csv-to-pdf.js');
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
console.log('[CSV2PDF] Converting single file:', originalFile.name, 'Size:', originalFile.size, 'bytes');
|
||||
|
||||
const pdfBlob = await convertCsvToPdf(originalFile, {
|
||||
onProgress: (percent, message) => {
|
||||
console.log(`[CSV2PDF] Progress: ${percent}% - ${message}`);
|
||||
showLoader(message, percent);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[CSV2PDF] Conversion complete! PDF size:', pdfBlob.size, 'bytes');
|
||||
|
||||
const fileName = originalFile.name.replace(/\.csv$/i, '') + '.pdf';
|
||||
downloadFile(pdfBlob, fileName);
|
||||
console.log('[CSV2PDF] File downloaded:', fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
console.log('[CSV2PDF] Converting multiple files:', state.files.length);
|
||||
showLoader('Preparing conversion...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
console.log(`[CSV2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name);
|
||||
|
||||
const pdfBlob = await convertCsvToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
const overallPercent = ((i / state.files.length) * 100) + (percent / state.files.length);
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`, overallPercent);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[CSV2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size);
|
||||
|
||||
const baseName = file.name.replace(/\.csv$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
console.log('[CSV2PDF] Generating ZIP file...');
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
console.log('[CSV2PDF] ZIP size:', zipBlob.size);
|
||||
|
||||
downloadFile(zipBlob, 'csv-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} CSV file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[CSV2PDF] ERROR:', e);
|
||||
console.error('[CSV2PDF] Error stack:', e.stack);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const csvFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.csv') || f.type === 'text/csv');
|
||||
if (csvFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
csvFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
@@ -1,21 +1,24 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
interface DividePagesState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const pageState: DividePagesState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
pageState.totalPages = 0;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
@@ -28,6 +31,9 @@ function resetState() {
|
||||
|
||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
||||
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
|
||||
|
||||
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
|
||||
if (pageRangeInput) pageRangeInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
@@ -73,10 +79,10 @@ async function updateUI() {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
pageState.totalPages = pageState.pdfDoc.getPageCount();
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.totalPages} pages`;
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
@@ -96,9 +102,25 @@ async function dividePages() {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
|
||||
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
|
||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
||||
const splitType = splitTypeSelect.value;
|
||||
|
||||
let pagesToDivide: Set<number>;
|
||||
|
||||
if (pageRangeValue === '' || pageRangeValue === 'all') {
|
||||
pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1));
|
||||
} else {
|
||||
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
|
||||
pagesToDivide = new Set(parsedIndices.map(i => i + 1));
|
||||
|
||||
if (pagesToDivide.size === 0) {
|
||||
showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Splitting PDF pages...');
|
||||
|
||||
try {
|
||||
@@ -106,27 +128,33 @@ async function dividePages() {
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const pageNum = i + 1;
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
|
||||
|
||||
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
if (pagesToDivide.has(pageNum)) {
|
||||
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(pageState.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);
|
||||
page2.setCropBox(0, 0, width, height / 2);
|
||||
break;
|
||||
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);
|
||||
page2.setCropBox(0, 0, width, height / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
} else {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
|
||||
@@ -4,8 +4,8 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { formatBytes } from '../utils/helpers.js';
|
||||
|
||||
const embedPdfWasmUrl = new URL(
|
||||
'embedpdf-snippet/dist/pdfium.wasm',
|
||||
import.meta.url
|
||||
'embedpdf-snippet/dist/pdfium.wasm',
|
||||
import.meta.url
|
||||
).href;
|
||||
|
||||
let currentPdfUrl: string | null = null;
|
||||
@@ -45,7 +45,6 @@ function initializePage() {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
@@ -81,11 +80,7 @@ async function handleFiles(files: FileList) {
|
||||
|
||||
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone || !fileDisplayArea) return;
|
||||
|
||||
// Hide uploader elements but keep the container
|
||||
// Hide uploader elements but keep the container
|
||||
// dropZone.classList.add('hidden');
|
||||
|
||||
// Show file display
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
@@ -114,7 +109,6 @@ async function handleFiles(files: FileList) {
|
||||
pdfContainer.textContent = '';
|
||||
pdfWrapper.classList.add('hidden');
|
||||
fileDisplayArea.innerHTML = '';
|
||||
// dropZone.classList.remove('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
@@ -123,13 +117,10 @@ async function handleFiles(files: FileList) {
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
// Clear previous content
|
||||
pdfContainer.textContent = '';
|
||||
if (currentPdfUrl) {
|
||||
URL.revokeObjectURL(currentPdfUrl);
|
||||
}
|
||||
|
||||
// Show editor container
|
||||
pdfWrapper.classList.remove('hidden');
|
||||
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
|
||||
205
src/js/logic/epub-to-pdf-page.ts
Normal file
205
src/js/logic/epub-to-pdf-page.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const FILETYPE = 'epub';
|
||||
const EXTENSIONS = ['.epub'];
|
||||
const TOOL_NAME = 'EPUB';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
|
||||
// ... (existing listeners)
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
if (convertOptions) convertOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
if (convertOptions) convertOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
216
src/js/logic/excel-to-pdf-page.ts
Normal file
216
src/js/logic/excel-to-pdf-page.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one Excel file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.(xls|xlsx|ods|csv)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.(xls|xlsx|ods|csv)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'excel-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} Excel file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const excelFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return name.endsWith('.xls') || name.endsWith('.xlsx') || name.endsWith('.ods') || name.endsWith('.csv');
|
||||
});
|
||||
if (excelFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
excelFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
280
src/js/logic/extract-images-page.ts
Normal file
280
src/js/logic/extract-images-page.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
interface ExtractedImage {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
ext: string;
|
||||
}
|
||||
|
||||
let extractedImages: ExtractedImage[] = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const extractOptions = document.getElementById('extract-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const imagesContainer = document.getElementById('images-container');
|
||||
const imagesGrid = document.getElementById('images-grid');
|
||||
const downloadAllBtn = document.getElementById('download-all-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
|
||||
|
||||
// Clear extracted images when files change
|
||||
extractedImages = [];
|
||||
if (imagesContainer) imagesContainer.classList.add('hidden');
|
||||
if (imagesGrid) imagesGrid.innerHTML = '';
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
extractOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
extractOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
extractedImages = [];
|
||||
if (imagesContainer) imagesContainer.classList.add('hidden');
|
||||
if (imagesGrid) imagesGrid.innerHTML = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const displayImages = () => {
|
||||
if (!imagesGrid || !imagesContainer) return;
|
||||
imagesGrid.innerHTML = '';
|
||||
|
||||
extractedImages.forEach((img, index) => {
|
||||
const blob = new Blob([new Uint8Array(img.data)]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-gray-700 rounded-lg overflow-hidden';
|
||||
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.src = url;
|
||||
imgEl.className = 'w-full h-32 object-cover';
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'p-2 flex justify-between items-center';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-xs text-gray-300 truncate';
|
||||
name.textContent = img.name;
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'text-indigo-400 hover:text-indigo-300';
|
||||
downloadBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4"></i>';
|
||||
downloadBtn.onclick = () => {
|
||||
downloadFile(blob, img.name);
|
||||
};
|
||||
|
||||
info.append(name, downloadBtn);
|
||||
card.append(imgEl, info);
|
||||
imagesGrid.appendChild(card);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
imagesContainer.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const extract = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF processor...');
|
||||
await pymupdf.load();
|
||||
|
||||
extractedImages = [];
|
||||
let imgCounter = 0;
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Extracting images from ${file.name}...`);
|
||||
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
|
||||
for (let pageIdx = 0; pageIdx < pageCount; pageIdx++) {
|
||||
const page = doc.getPage(pageIdx);
|
||||
const images = page.getImages();
|
||||
|
||||
for (const imgInfo of images) {
|
||||
try {
|
||||
const imgData = page.extractImage(imgInfo.xref);
|
||||
if (imgData && imgData.data) {
|
||||
imgCounter++;
|
||||
extractedImages.push({
|
||||
data: imgData.data,
|
||||
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
|
||||
ext: imgData.ext || 'png'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to extract image:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.close();
|
||||
}
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (extractedImages.length === 0) {
|
||||
showAlert('No Images Found', 'No embedded images were found in the selected PDF(s).');
|
||||
} else {
|
||||
displayImages();
|
||||
showAlert(
|
||||
'Extraction Complete',
|
||||
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAll = async () => {
|
||||
if (extractedImages.length === 0) return;
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
extractedImages.forEach((img) => {
|
||||
zip.file(img.name, img.data);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-images.zip');
|
||||
hideLoader();
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(
|
||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
state.files = [...state.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extract);
|
||||
}
|
||||
|
||||
if (downloadAllBtn) {
|
||||
downloadAllBtn.addEventListener('click', downloadAll);
|
||||
}
|
||||
});
|
||||
240
src/js/logic/extract-tables-page.ts
Normal file
240
src/js/logic/extract-tables-page.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (file) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
file = null;
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
function tableToCsv(rows: (string | null)[][]): string {
|
||||
return rows.map(row =>
|
||||
row.map(cell => {
|
||||
const cellStr = cell ?? '';
|
||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return cellStr;
|
||||
}).join(',')
|
||||
).join('\n');
|
||||
}
|
||||
|
||||
async function extract() {
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formatRadios = document.querySelectorAll('input[name="export-format"]');
|
||||
let format = 'csv';
|
||||
formatRadios.forEach((radio: Element) => {
|
||||
if ((radio as HTMLInputElement).checked) {
|
||||
format = (radio as HTMLInputElement).value;
|
||||
}
|
||||
});
|
||||
|
||||
showLoader('Loading Engine...');
|
||||
|
||||
try {
|
||||
await pymupdf.load();
|
||||
showLoader('Extracting tables...');
|
||||
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
interface TableData {
|
||||
page: number;
|
||||
tableIndex: number;
|
||||
rows: (string | null)[][];
|
||||
markdown: string;
|
||||
rowCount: number;
|
||||
colCount: number;
|
||||
}
|
||||
|
||||
const allTables: TableData[] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
tables.forEach((table, tableIdx) => {
|
||||
allTables.push({
|
||||
page: i + 1,
|
||||
tableIndex: tableIdx + 1,
|
||||
rows: table.rows,
|
||||
markdown: table.markdown,
|
||||
rowCount: table.rowCount,
|
||||
colCount: table.colCount
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (allTables.length === 0) {
|
||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (allTables.length === 1) {
|
||||
const table = allTables[0];
|
||||
let content: string;
|
||||
let ext: string;
|
||||
let mimeType: string;
|
||||
|
||||
if (format === 'csv') {
|
||||
content = tableToCsv(table.rows);
|
||||
ext = 'csv';
|
||||
mimeType = 'text/csv';
|
||||
} else if (format === 'json') {
|
||||
content = JSON.stringify(table.rows, null, 2);
|
||||
ext = 'json';
|
||||
mimeType = 'application/json';
|
||||
} else {
|
||||
content = table.markdown;
|
||||
ext = 'md';
|
||||
mimeType = 'text/markdown';
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
downloadFile(blob, `${baseName}_table.${ext}`);
|
||||
showAlert('Success', `Extracted 1 table successfully!`, 'success', resetState);
|
||||
} else {
|
||||
showLoader('Creating ZIP file...');
|
||||
const zip = new JSZip();
|
||||
|
||||
allTables.forEach((table, idx) => {
|
||||
const filename = `table_${idx + 1}_page${table.page}`;
|
||||
let content: string;
|
||||
let ext: string;
|
||||
|
||||
if (format === 'csv') {
|
||||
content = tableToCsv(table.rows);
|
||||
ext = 'csv';
|
||||
} else if (format === 'json') {
|
||||
content = JSON.stringify(table.rows, null, 2);
|
||||
ext = 'json';
|
||||
} else {
|
||||
content = table.markdown;
|
||||
ext = 'md';
|
||||
}
|
||||
|
||||
zip.file(`${filename}.${ext}`, content);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
||||
showAlert('Success', `Extracted ${allTables.length} tables successfully!`, 'success', resetState);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to extract tables. ${message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
||||
|
||||
if (!validFile) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
file = validFile;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extract);
|
||||
}
|
||||
});
|
||||
201
src/js/logic/fb2-to-pdf-page.ts
Normal file
201
src/js/logic/fb2-to-pdf-page.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const FILETYPE = 'fb2';
|
||||
const EXTENSIONS = ['.fb2'];
|
||||
const TOOL_NAME = 'FB2';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const SUPPORTED_FORMATS = '.jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.pnm,.pgm,.pbm,.ppm,.pam,.jxr,.jpx,.jp2,.psd,.svg,.heic,.heif,.webp';
|
||||
const SUPPORTED_FORMATS_DISPLAY = 'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDF | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -19,8 +23,14 @@ function initializePage() {
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const formatDisplay = document.getElementById('supported-formats');
|
||||
|
||||
if (formatDisplay) {
|
||||
formatDisplay.textContent = SUPPORTED_FORMATS_DISPLAY;
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.accept = SUPPORTED_FORMATS;
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
|
||||
@@ -43,7 +53,6 @@ function initializePage() {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
@@ -78,13 +87,21 @@ function handleFileUpload(e: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function getFileExtension(filename: string): string {
|
||||
return '.' + filename.split('.').pop()?.toLowerCase() || '';
|
||||
}
|
||||
|
||||
function isValidImageFile(file: File): boolean {
|
||||
const ext = getFileExtension(file.name);
|
||||
const validExtensions = SUPPORTED_FORMATS.split(',');
|
||||
return validExtensions.includes(ext) || file.type.startsWith('image/');
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
const validFiles = Array.from(newFiles).filter(file =>
|
||||
file.type.startsWith('image/')
|
||||
);
|
||||
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only image files are allowed.');
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only supported image formats are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
@@ -146,95 +163,12 @@ function updateUI() {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
return reject(new Error('Could not get canvas context'));
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
// Special handler for SVG files - must read as text
|
||||
function svgToPng(svgText: string): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const width = img.naturalWidth || img.width || 800;
|
||||
const height = img.naturalHeight || img.height || 600;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(url);
|
||||
return reject(new Error('Could not get canvas context'));
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
canvas.toBlob(
|
||||
async (pngBlob) => {
|
||||
URL.revokeObjectURL(url);
|
||||
if (!pngBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await pngBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/png'
|
||||
);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('Failed to load SVG image'));
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
||||
if (!pymupdf) {
|
||||
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
}
|
||||
return pymupdf;
|
||||
}
|
||||
|
||||
async function convertToPdf() {
|
||||
@@ -243,78 +177,23 @@ async function convertToPdf() {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating PDF from images...');
|
||||
showLoader('Loading PyMuPDF engine...');
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const mupdf = await ensurePyMuPDF();
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg');
|
||||
showLoader('Converting images to PDF...');
|
||||
|
||||
if (isSvg) {
|
||||
// Handle SVG files - read as text
|
||||
const svgText = await file.text();
|
||||
const pngBytes = await svgToPng(svgText);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const pdfBlob = await mupdf.imagesToPdf(files);
|
||||
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
} else if (file.type === 'image/png') {
|
||||
// Handle PNG files
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(originalBytes as Uint8Array);
|
||||
downloadFile(pdfBlob, 'images_to_pdf.pdf');
|
||||
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
} else {
|
||||
// Handle JPG/other raster images
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
// Fallback: convert to JPEG via canvas
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to process ${file.name}:`, error);
|
||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_images.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
console.error('[ImageToPDF]', e);
|
||||
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx';
|
||||
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDF | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -43,7 +47,6 @@ function initializePage() {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
@@ -78,13 +81,21 @@ function handleFileUpload(e: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function getFileExtension(filename: string): string {
|
||||
return '.' + (filename.split('.').pop()?.toLowerCase() || '');
|
||||
}
|
||||
|
||||
function isValidImageFile(file: File): boolean {
|
||||
const ext = getFileExtension(file.name);
|
||||
const validExtensions = SUPPORTED_FORMATS.split(',');
|
||||
return validExtensions.includes(ext) || SUPPORTED_MIME_TYPES.includes(file.type);
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
const validFiles = Array.from(newFiles).filter(file =>
|
||||
file.type === 'image/jpeg' || file.type === 'image/jpg' || file.name.toLowerCase().endsWith('.jpg') || file.name.toLowerCase().endsWith('.jpeg')
|
||||
);
|
||||
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only JPG/JPEG images are allowed.');
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only JPG, JPEG, JP2, and JPX files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
@@ -146,102 +157,37 @@ function updateUI() {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
||||
if (!pymupdf) {
|
||||
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
}
|
||||
return pymupdf;
|
||||
}
|
||||
|
||||
async function convertToPdf() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one JPG file.');
|
||||
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating PDF from JPGs...');
|
||||
showLoader('Loading PyMuPDF engine...');
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const mupdf = await ensurePyMuPDF();
|
||||
|
||||
for (const file of files) {
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
showLoader('Converting images to PDF...');
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
showAlert(
|
||||
'Warning',
|
||||
`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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
const pdfBlob = await mupdf.imagesToPdf(files);
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
downloadFile(pdfBlob, 'from_jpgs.pdf');
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_jpgs.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
console.error('[JpgToPdf]', e);
|
||||
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
21
src/js/logic/markdown-to-pdf-page.ts
Normal file
21
src/js/logic/markdown-to-pdf-page.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { MarkdownEditor } from '../utils/markdown-editor.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('markdown-editor-container');
|
||||
|
||||
if (!container) {
|
||||
console.error('Markdown editor container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = new MarkdownEditor(container, {});
|
||||
|
||||
console.log('Markdown editor initialized');
|
||||
|
||||
const backButton = document.getElementById('back-to-tools');
|
||||
if (backButton) {
|
||||
backButton.addEventListener('click', () => {
|
||||
window.location.href = '/';
|
||||
});
|
||||
}
|
||||
});
|
||||
201
src/js/logic/mobi-to-pdf-page.ts
Normal file
201
src/js/logic/mobi-to-pdf-page.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const FILETYPE = 'mobi';
|
||||
const EXTENSIONS = ['.mobi'];
|
||||
const TOOL_NAME = 'MOBI';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
189
src/js/logic/odg-to-pdf-page.ts
Normal file
189
src/js/logic/odg-to-pdf-page.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.odg'];
|
||||
const FILETYPE_NAME = 'ODG';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
189
src/js/logic/odp-to-pdf-page.ts
Normal file
189
src/js/logic/odp-to-pdf-page.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.odp'];
|
||||
const FILETYPE_NAME = 'ODP';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
189
src/js/logic/ods-to-pdf-page.ts
Normal file
189
src/js/logic/ods-to-pdf-page.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.ods'];
|
||||
const FILETYPE_NAME = 'ODS';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
215
src/js/logic/odt-to-pdf-page.ts
Normal file
215
src/js/logic/odt-to-pdf-page.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one ODT file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.odt$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.odt$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'odt-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ODT file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const odtFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.odt') || f.type === 'application/vnd.oasis.opendocument.text');
|
||||
if (odtFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
odtFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
188
src/js/logic/pages-to-pdf-page.ts
Normal file
188
src/js/logic/pages-to-pdf-page.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.pages'];
|
||||
const FILETYPE_NAME = 'Pages';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
517
src/js/logic/pdf-booklet-page.ts
Normal file
517
src/js/logic/pdf-booklet-page.ts
Normal file
@@ -0,0 +1,517 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
interface BookletState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
pdfBytes: Uint8Array | null;
|
||||
pdfjsDoc: pdfjsLib.PDFDocumentProxy | null;
|
||||
}
|
||||
|
||||
const pageState: BookletState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
pdfBytes: null,
|
||||
pdfjsDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
pageState.pdfBytes = null;
|
||||
pageState.pdfjsDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const previewArea = document.getElementById('booklet-preview');
|
||||
if (previewArea) previewArea.innerHTML = '<p class="text-gray-400 text-center py-8">Upload a PDF and click "Generate Preview" to see the booklet layout</p>';
|
||||
|
||||
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
|
||||
if (downloadBtn) downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfBytes = new Uint8Array(arrayBuffer);
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
|
||||
pageState.pdfjsDoc = await pdfjsLib.getDocument({ data: pageState.pdfBytes.slice() }).promise;
|
||||
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
|
||||
const previewBtn = document.getElementById('preview-btn') as HTMLButtonElement;
|
||||
if (previewBtn) previewBtn.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function getGridDimensions(): { rows: number; cols: number } {
|
||||
const gridMode = (document.querySelector('input[name="grid-mode"]:checked') as HTMLInputElement)?.value || '1x2';
|
||||
switch (gridMode) {
|
||||
case '1x2': return { rows: 1, cols: 2 };
|
||||
case '2x2': return { rows: 2, cols: 2 };
|
||||
case '2x4': return { rows: 2, cols: 4 };
|
||||
case '4x4': return { rows: 4, cols: 4 };
|
||||
default: return { rows: 1, cols: 2 };
|
||||
}
|
||||
}
|
||||
|
||||
function getOrientation(isBookletMode: boolean): 'portrait' | 'landscape' {
|
||||
const orientationValue = (document.querySelector('input[name="orientation"]:checked') as HTMLInputElement)?.value || 'auto';
|
||||
if (orientationValue === 'portrait') return 'portrait';
|
||||
if (orientationValue === 'landscape') return 'landscape';
|
||||
return isBookletMode ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
function getSheetDimensions(isBookletMode: boolean): { width: number; height: number } {
|
||||
const paperSizeKey = (document.getElementById('paper-size') as HTMLSelectElement).value as keyof typeof PageSizes;
|
||||
const pageDims = PageSizes[paperSizeKey] || PageSizes.Letter;
|
||||
const orientation = getOrientation(isBookletMode);
|
||||
if (orientation === 'landscape') {
|
||||
return { width: pageDims[1], height: pageDims[0] };
|
||||
}
|
||||
return { width: pageDims[0], height: pageDims[1] };
|
||||
}
|
||||
|
||||
async function generatePreview() {
|
||||
if (!pageState.pdfDoc || !pageState.pdfjsDoc) {
|
||||
showAlert('Error', 'Please load a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const previewArea = document.getElementById('booklet-preview')!;
|
||||
const totalPages = pageState.pdfDoc.getPageCount();
|
||||
const { rows, cols } = getGridDimensions();
|
||||
const pagesPerSheet = rows * cols;
|
||||
const isBookletMode = rows === 1 && cols === 2;
|
||||
|
||||
let numSheets: number;
|
||||
if (isBookletMode) {
|
||||
const sheetsNeeded = Math.ceil(totalPages / 4);
|
||||
numSheets = sheetsNeeded * 2;
|
||||
} else {
|
||||
numSheets = Math.ceil(totalPages / pagesPerSheet);
|
||||
}
|
||||
|
||||
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
|
||||
|
||||
// Get container width to make canvas fill it
|
||||
const previewContainer = document.getElementById('booklet-preview')!;
|
||||
const containerWidth = previewContainer.clientWidth - 32; // account for padding
|
||||
const aspectRatio = sheetWidth / sheetHeight;
|
||||
const canvasWidth = containerWidth;
|
||||
const canvasHeight = containerWidth / aspectRatio;
|
||||
|
||||
previewArea.innerHTML = '<p class="text-gray-400 text-center py-4">Generating preview...</p>';
|
||||
|
||||
const totalRounded = isBookletMode ? Math.ceil(totalPages / 4) * 4 : totalPages;
|
||||
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
|
||||
|
||||
const pageThumbnails: Map<number, ImageBitmap> = new Map();
|
||||
const thumbnailScale = 0.3;
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
try {
|
||||
const page = await pageState.pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: thumbnailScale });
|
||||
|
||||
const offscreen = new OffscreenCanvas(viewport.width, viewport.height);
|
||||
const ctx = offscreen.getContext('2d')!;
|
||||
|
||||
await page.render({
|
||||
canvasContext: ctx as any,
|
||||
viewport: viewport,
|
||||
canvas: offscreen as any,
|
||||
}).promise;
|
||||
|
||||
const bitmap = await createImageBitmap(offscreen);
|
||||
pageThumbnails.set(i, bitmap);
|
||||
} catch (e) {
|
||||
console.error(`Failed to render page ${i}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
previewArea.innerHTML = `<p class="text-indigo-400 text-sm mb-4 text-center">${totalPages} pages → ${numSheets} output sheets</p>`;
|
||||
|
||||
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
canvas.className = 'border border-gray-600 rounded-lg mb-4';
|
||||
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
const isFront = sheetIndex % 2 === 0;
|
||||
ctx.fillStyle = isFront ? '#1f2937' : '#1a2e1a';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
ctx.strokeStyle = '#4b5563';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
const cellWidth = canvasWidth / cols;
|
||||
const cellHeight = canvasHeight / rows;
|
||||
const padding = 4;
|
||||
|
||||
ctx.strokeStyle = '#374151';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([2, 2]);
|
||||
for (let c = 1; c < cols; c++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c * cellWidth, 0);
|
||||
ctx.lineTo(c * cellWidth, canvasHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let r = 1; r < rows; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * cellHeight);
|
||||
ctx.lineTo(canvasWidth, r * cellHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const slotIndex = r * cols + c;
|
||||
let pageNumber: number;
|
||||
|
||||
if (isBookletMode) {
|
||||
const physicalSheet = Math.floor(sheetIndex / 2);
|
||||
const isFrontSide = sheetIndex % 2 === 0;
|
||||
if (isFrontSide) {
|
||||
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
|
||||
} else {
|
||||
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
|
||||
}
|
||||
} else {
|
||||
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
|
||||
}
|
||||
|
||||
const x = c * cellWidth + padding;
|
||||
const y = r * cellHeight + padding;
|
||||
const slotWidth = cellWidth - padding * 2;
|
||||
const slotHeight = cellHeight - padding * 2;
|
||||
|
||||
const exists = pageNumber >= 1 && pageNumber <= totalPages;
|
||||
|
||||
if (exists) {
|
||||
const thumbnail = pageThumbnails.get(pageNumber);
|
||||
if (thumbnail) {
|
||||
let rotation = 0;
|
||||
if (rotationMode === '90cw') rotation = 90;
|
||||
else if (rotationMode === '90ccw') rotation = -90;
|
||||
else if (rotationMode === 'alternate') rotation = (pageNumber % 2 === 1) ? 90 : -90;
|
||||
|
||||
const isRotated = rotation !== 0;
|
||||
const srcWidth = isRotated ? thumbnail.height : thumbnail.width;
|
||||
const srcHeight = isRotated ? thumbnail.width : thumbnail.height;
|
||||
const scale = Math.min(slotWidth / srcWidth, slotHeight / srcHeight);
|
||||
const drawWidth = srcWidth * scale;
|
||||
const drawHeight = srcHeight * scale;
|
||||
const drawX = x + (slotWidth - drawWidth) / 2;
|
||||
const drawY = y + (slotHeight - drawHeight) / 2;
|
||||
|
||||
ctx.save();
|
||||
if (rotation !== 0) {
|
||||
const centerX = drawX + drawWidth / 2;
|
||||
const centerY = drawY + drawHeight / 2;
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate((rotation * Math.PI) / 180);
|
||||
ctx.drawImage(thumbnail, -drawHeight / 2, -drawWidth / 2, drawHeight, drawWidth);
|
||||
} else {
|
||||
ctx.drawImage(thumbnail, drawX, drawY, drawWidth, drawHeight);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
ctx.strokeStyle = '#6b7280';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(drawX, drawY, drawWidth, drawHeight);
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${pageNumber}`, x + slotWidth / 2, y + slotHeight - 4);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = '#374151';
|
||||
ctx.fillRect(x, y, slotWidth, slotHeight);
|
||||
ctx.strokeStyle = '#4b5563';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y, slotWidth, slotHeight);
|
||||
|
||||
ctx.fillStyle = '#6b7280';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('(blank)', x + slotWidth / 2, y + slotHeight / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#9ca3af';
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'top';
|
||||
const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : '';
|
||||
ctx.fillText(`Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`, canvasWidth - 6, 4);
|
||||
|
||||
previewArea.appendChild(canvas);
|
||||
}
|
||||
|
||||
pageThumbnails.forEach(bitmap => bitmap.close());
|
||||
|
||||
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
function applyRotation(doc: PDFLibDocument, mode: string) {
|
||||
const pages = doc.getPages();
|
||||
pages.forEach((page, index) => {
|
||||
let rotation = 0;
|
||||
switch (mode) {
|
||||
case '90cw': rotation = 90; break;
|
||||
case '90ccw': rotation = -90; break;
|
||||
case 'alternate': rotation = (index % 2 === 0) ? 90 : -90; break;
|
||||
default: rotation = 0;
|
||||
}
|
||||
if (rotation !== 0) {
|
||||
page.setRotation(degrees(page.getRotation().angle + rotation));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createBooklet() {
|
||||
if (!pageState.pdfBytes) {
|
||||
showAlert('Error', 'Please load a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating Booklet...');
|
||||
|
||||
try {
|
||||
const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice());
|
||||
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
|
||||
applyRotation(sourceDoc, rotationMode);
|
||||
|
||||
const totalPages = sourceDoc.getPageCount();
|
||||
const { rows, cols } = getGridDimensions();
|
||||
const pagesPerSheet = rows * cols;
|
||||
const isBookletMode = rows === 1 && cols === 2;
|
||||
|
||||
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
|
||||
|
||||
const outputDoc = await PDFLibDocument.create();
|
||||
|
||||
let numSheets: number;
|
||||
let totalRounded: number;
|
||||
if (isBookletMode) {
|
||||
totalRounded = Math.ceil(totalPages / 4) * 4;
|
||||
numSheets = Math.ceil(totalPages / 4) * 2;
|
||||
} else {
|
||||
totalRounded = totalPages;
|
||||
numSheets = Math.ceil(totalPages / pagesPerSheet);
|
||||
}
|
||||
|
||||
const cellWidth = sheetWidth / cols;
|
||||
const cellHeight = sheetHeight / rows;
|
||||
const padding = 10;
|
||||
|
||||
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
|
||||
const outputPage = outputDoc.addPage([sheetWidth, sheetHeight]);
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const slotIndex = r * cols + c;
|
||||
let pageNumber: number;
|
||||
|
||||
if (isBookletMode) {
|
||||
const physicalSheet = Math.floor(sheetIndex / 2);
|
||||
const isFrontSide = sheetIndex % 2 === 0;
|
||||
if (isFrontSide) {
|
||||
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
|
||||
} else {
|
||||
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
|
||||
}
|
||||
} else {
|
||||
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
|
||||
}
|
||||
|
||||
if (pageNumber >= 1 && pageNumber <= totalPages) {
|
||||
const [embeddedPage] = await outputDoc.embedPdf(sourceDoc, [pageNumber - 1]);
|
||||
const { width: srcW, height: srcH } = embeddedPage;
|
||||
|
||||
const availableWidth = cellWidth - padding * 2;
|
||||
const availableHeight = cellHeight - padding * 2;
|
||||
const scale = Math.min(availableWidth / srcW, availableHeight / srcH);
|
||||
|
||||
const scaledWidth = srcW * scale;
|
||||
const scaledHeight = srcH * scale;
|
||||
|
||||
const x = c * cellWidth + padding + (availableWidth - scaledWidth) / 2;
|
||||
const y = sheetHeight - (r + 1) * cellHeight + padding + (availableHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await outputDoc.save();
|
||||
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_booklet.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', `Booklet created with ${numSheets} sheets!`, 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the booklet.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const previewBtn = document.getElementById('preview-btn');
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (previewBtn) {
|
||||
previewBtn.addEventListener('click', generatePreview);
|
||||
}
|
||||
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', createBooklet);
|
||||
}
|
||||
});
|
||||
415
src/js/logic/pdf-layers-page.ts
Normal file
415
src/js/logic/pdf-layers-page.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
interface LayerData {
|
||||
number: number;
|
||||
xref: number;
|
||||
text: string;
|
||||
on: boolean;
|
||||
locked: boolean;
|
||||
depth: number;
|
||||
parentXref: number;
|
||||
displayOrder: number;
|
||||
};
|
||||
|
||||
let currentFile: File | null = null;
|
||||
let currentDoc: any = null;
|
||||
let layersMap = new Map<number, LayerData>();
|
||||
let nextDisplayOrder = 0;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const processBtnContainer = document.getElementById('process-btn-container');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const layersContainer = document.getElementById('layers-container');
|
||||
const layersList = document.getElementById('layers-list');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtnContainer || !processBtn) return;
|
||||
|
||||
if (currentFile) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = currentFile.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
processBtnContainer.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
processBtnContainer.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
currentFile = null;
|
||||
currentDoc = null;
|
||||
layersMap.clear();
|
||||
nextDisplayOrder = 0;
|
||||
|
||||
if (dropZone) dropZone.style.display = 'flex';
|
||||
if (layersContainer) layersContainer.classList.add('hidden');
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const promptForInput = (title: string, message: string, defaultValue: string = ''): Promise<string | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById('input-modal');
|
||||
const titleEl = document.getElementById('input-title');
|
||||
const messageEl = document.getElementById('input-message');
|
||||
const inputEl = document.getElementById('input-value') as HTMLInputElement;
|
||||
const confirmBtn = document.getElementById('input-confirm');
|
||||
const cancelBtn = document.getElementById('input-cancel');
|
||||
|
||||
if (!modal || !titleEl || !messageEl || !inputEl || !confirmBtn || !cancelBtn) {
|
||||
console.error('Input modal elements not found');
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
titleEl.textContent = title;
|
||||
messageEl.textContent = message;
|
||||
inputEl.value = defaultValue;
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.add('hidden');
|
||||
confirmBtn.onclick = null;
|
||||
cancelBtn.onclick = null;
|
||||
inputEl.onkeydown = null;
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
const val = inputEl.value.trim();
|
||||
closeModal();
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
closeModal();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
confirmBtn.onclick = confirm;
|
||||
cancelBtn.onclick = cancel;
|
||||
|
||||
inputEl.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') confirm();
|
||||
if (e.key === 'Escape') cancel();
|
||||
};
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
inputEl.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const renderLayers = () => {
|
||||
if (!layersList) return;
|
||||
|
||||
const layersArray = Array.from(layersMap.values());
|
||||
|
||||
if (layersArray.length === 0) {
|
||||
layersList.innerHTML = `
|
||||
<div class="layers-empty">
|
||||
<p>This PDF has no layers (OCG).</p>
|
||||
<p>Add a new layer to get started!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort layers by displayOrder
|
||||
const sortedLayers = layersArray.sort((a, b) => a.displayOrder - b.displayOrder);
|
||||
|
||||
layersList.innerHTML = sortedLayers.map((layer: LayerData) => `
|
||||
<div class="layer-item" data-number="${layer.number}" style="padding-left: ${layer.depth * 24 + 8}px;">
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" ${layer.on ? 'checked' : ''} ${layer.locked ? 'disabled' : ''} data-xref="${layer.xref}" />
|
||||
<span class="layer-name">${layer.depth > 0 ? '└ ' : ''}${layer.text || `Layer ${layer.number}`}</span>
|
||||
${layer.locked ? '<span class="layer-locked">🔒</span>' : ''}
|
||||
</label>
|
||||
<div class="layer-actions">
|
||||
${!layer.locked ? `<button class="layer-add-child" data-xref="${layer.xref}" title="Add child layer">+</button>` : ''}
|
||||
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="Delete layer">✕</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Attach toggle handlers
|
||||
layersList.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const xref = parseInt(target.dataset.xref || '0');
|
||||
const isOn = target.checked;
|
||||
|
||||
try {
|
||||
currentDoc.setLayerVisibility(xref, isOn);
|
||||
const layer = Array.from(layersMap.values()).find(l => l.xref === xref);
|
||||
if (layer) {
|
||||
layer.on = isOn;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to set layer visibility:', err);
|
||||
target.checked = !isOn;
|
||||
showAlert('Error', 'Failed to toggle layer visibility');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Attach delete handlers
|
||||
layersList.querySelectorAll('.layer-delete').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLButtonElement;
|
||||
const xref = parseInt(target.dataset.xref || '0');
|
||||
const layer = Array.from(layersMap.values()).find(l => l.xref === xref);
|
||||
|
||||
if (!layer) {
|
||||
showAlert('Error', 'Layer not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentDoc.deleteOCG(layer.number);
|
||||
layersMap.delete(layer.number);
|
||||
renderLayers();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete layer:', err);
|
||||
showAlert('Error', 'Failed to delete layer');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
layersList.querySelectorAll('.layer-add-child').forEach((btn) => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const target = e.target as HTMLButtonElement;
|
||||
const parentXref = parseInt(target.dataset.xref || '0');
|
||||
const parentLayer = Array.from(layersMap.values()).find(l => l.xref === parentXref);
|
||||
|
||||
const childName = await promptForInput('Add Child Layer', `Enter name for child layer under "${parentLayer?.text || 'Layer'}":`);
|
||||
|
||||
if (!childName || !childName.trim()) return;
|
||||
|
||||
try {
|
||||
const childXref = currentDoc.addOCGWithParent(childName.trim(), parentXref);
|
||||
const parentDisplayOrder = parentLayer?.displayOrder || 0;
|
||||
layersMap.forEach((l) => {
|
||||
if (l.displayOrder > parentDisplayOrder) {
|
||||
l.displayOrder += 1;
|
||||
}
|
||||
});
|
||||
|
||||
layersMap.set(childXref, {
|
||||
number: childXref,
|
||||
xref: childXref,
|
||||
text: childName.trim(),
|
||||
on: true,
|
||||
locked: false,
|
||||
depth: (parentLayer?.depth || 0) + 1,
|
||||
parentXref: parentXref,
|
||||
displayOrder: parentDisplayOrder + 1
|
||||
});
|
||||
|
||||
renderLayers();
|
||||
} catch (err) {
|
||||
console.error('Failed to add child layer:', err);
|
||||
showAlert('Error', 'Failed to add child layer');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const loadLayers = async () => {
|
||||
if (!currentFile) {
|
||||
showAlert('No File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Loading PyMuPDF...');
|
||||
await pymupdf.load();
|
||||
|
||||
showLoader(`Loading layers from ${currentFile.name}...`);
|
||||
currentDoc = await pymupdf.open(currentFile);
|
||||
|
||||
showLoader('Reading layer configuration...');
|
||||
const existingLayers = currentDoc.getLayerConfig();
|
||||
|
||||
// Reset and populate layers map
|
||||
layersMap.clear();
|
||||
nextDisplayOrder = 0;
|
||||
|
||||
existingLayers.forEach((layer: any) => {
|
||||
layersMap.set(layer.number, {
|
||||
number: layer.number,
|
||||
xref: layer.xref ?? layer.number,
|
||||
text: layer.text,
|
||||
on: layer.on,
|
||||
locked: layer.locked,
|
||||
depth: layer.depth ?? 0,
|
||||
parentXref: layer.parentXref ?? 0,
|
||||
displayOrder: layer.displayOrder ?? nextDisplayOrder++
|
||||
});
|
||||
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
|
||||
nextDisplayOrder = layer.displayOrder + 1;
|
||||
}
|
||||
});
|
||||
|
||||
hideLoader();
|
||||
|
||||
// Hide upload zone, show layers container
|
||||
if (dropZone) dropZone.style.display = 'none';
|
||||
if (processBtnContainer) processBtnContainer.classList.add('hidden');
|
||||
if (layersContainer) layersContainer.classList.remove('hidden');
|
||||
|
||||
renderLayers();
|
||||
setupLayerHandlers();
|
||||
|
||||
} catch (error: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', error.message || 'Failed to load PDF layers');
|
||||
console.error('Layers error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const setupLayerHandlers = () => {
|
||||
const addLayerBtn = document.getElementById('add-layer-btn');
|
||||
const newLayerInput = document.getElementById('new-layer-name') as HTMLInputElement;
|
||||
const saveLayersBtn = document.getElementById('save-layers-btn');
|
||||
|
||||
if (addLayerBtn && newLayerInput) {
|
||||
addLayerBtn.onclick = () => {
|
||||
const name = newLayerInput.value.trim();
|
||||
if (!name) {
|
||||
showAlert('Invalid Name', 'Please enter a layer name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const xref = currentDoc.addOCG(name);
|
||||
newLayerInput.value = '';
|
||||
|
||||
const newDisplayOrder = nextDisplayOrder++;
|
||||
layersMap.set(xref, {
|
||||
number: xref,
|
||||
xref: xref,
|
||||
text: name,
|
||||
on: true,
|
||||
locked: false,
|
||||
depth: 0,
|
||||
parentXref: 0,
|
||||
displayOrder: newDisplayOrder
|
||||
});
|
||||
|
||||
renderLayers();
|
||||
} catch (err: any) {
|
||||
showAlert('Error', 'Failed to add layer: ' + err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (saveLayersBtn) {
|
||||
saveLayersBtn.onclick = () => {
|
||||
try {
|
||||
showLoader('Saving PDF with layer changes...');
|
||||
const pdfBytes = currentDoc.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
||||
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
|
||||
downloadFile(blob, outName);
|
||||
hideLoader();
|
||||
resetState();
|
||||
showAlert('Success', 'PDF with layer changes saved!', 'success');
|
||||
} catch (err: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to save PDF: ' + err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
currentFile = file;
|
||||
updateUI();
|
||||
} else {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', loadLayers);
|
||||
}
|
||||
});
|
||||
171
src/js/logic/pdf-to-csv-page.ts
Normal file
171
src/js/logic/pdf-to-csv-page.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (file) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
file = null;
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
function tableToCsv(rows: (string | null)[][]): string {
|
||||
return rows.map(row =>
|
||||
row.map(cell => {
|
||||
const cellStr = cell ?? '';
|
||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return cellStr;
|
||||
}).join(',')
|
||||
).join('\n');
|
||||
}
|
||||
|
||||
async function convert() {
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading Engine...');
|
||||
|
||||
try {
|
||||
await pymupdf.load();
|
||||
showLoader('Extracting tables...');
|
||||
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
const allRows: (string | null)[][] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
tables.forEach((table) => {
|
||||
allRows.push(...table.rows);
|
||||
allRows.push([]);
|
||||
});
|
||||
}
|
||||
|
||||
if (allRows.length === 0) {
|
||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
const csvContent = tableToCsv(allRows.filter(row => row.length > 0));
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
downloadFile(blob, `${baseName}.csv`);
|
||||
showAlert('Success', 'PDF converted to CSV successfully!', 'success', resetState);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
||||
|
||||
if (!validFile) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
file = validFile;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
202
src/js/logic/pdf-to-docx-page.ts
Normal file
202
src/js/logic/pdf-to-docx-page.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF converter...');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
|
||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '.docx';
|
||||
|
||||
downloadFile(docxBlob, outName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${file.name} to DOCX.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple PDFs...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
const arrayBuffer = await docxBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.docx`, arrayBuffer);
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'converted-documents.zip');
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(
|
||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
state.files = [...state.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
181
src/js/logic/pdf-to-excel-page.ts
Normal file
181
src/js/logic/pdf-to-excel-page.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (file) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
file = null;
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading Engine...');
|
||||
|
||||
try {
|
||||
await pymupdf.load();
|
||||
showLoader('Extracting tables...');
|
||||
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
interface TableData {
|
||||
page: number;
|
||||
rows: (string | null)[][];
|
||||
}
|
||||
|
||||
const allTables: TableData[] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
tables.forEach((table) => {
|
||||
allTables.push({
|
||||
page: i + 1,
|
||||
rows: table.rows
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (allTables.length === 0) {
|
||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating Excel file...');
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
if (allTables.length === 1) {
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(allTables[0].rows);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
||||
} else {
|
||||
allTables.forEach((table, idx) => {
|
||||
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(0, 31);
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
});
|
||||
}
|
||||
|
||||
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
const blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
downloadFile(blob, `${baseName}.xlsx`);
|
||||
showAlert('Success', `Extracted ${allTables.length} table(s) to Excel!`, 'success', resetState);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
||||
|
||||
if (!validFile) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
file = validFile;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
205
src/js/logic/pdf-to-markdown-page.ts
Normal file
205
src/js/logic/pdf-to-markdown-page.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const includeImagesCheckbox = document.getElementById('include-images') as HTMLInputElement;
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF converter...');
|
||||
await pymupdf.load();
|
||||
|
||||
const includeImages = includeImagesCheckbox?.checked ?? false;
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
|
||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
|
||||
downloadFile(blob, outName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${file.name} to Markdown.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple PDFs...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
zip.file(`${baseName}.md`, markdown);
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'markdown-files.zip');
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(
|
||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
state.files = [...state.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
228
src/js/logic/pdf-to-pdfa-page.ts
Normal file
228
src/js/logic/pdf-to-pdfa-page.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsContainer = document.getElementById('options-container');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const pdfaLevelSelect = document.getElementById('pdfa-level') as HTMLSelectElement;
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
optionsContainer.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
optionsContainer.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
|
||||
if (pdfaLevelSelect) pdfaLevelSelect.value = 'PDF/A-2b';
|
||||
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdfA = async () => {
|
||||
const level = pdfaLevelSelect.value as PdfALevel;
|
||||
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Initializing Ghostscript...');
|
||||
|
||||
const convertedBlob = await convertFileToPdfA(
|
||||
originalFile,
|
||||
level,
|
||||
(msg) => showLoader(msg)
|
||||
);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
|
||||
|
||||
downloadFile(convertedBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to ${level}.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple PDFs to PDF/A...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const convertedBlob = await convertFileToPdfA(
|
||||
file,
|
||||
level,
|
||||
(msg) => showLoader(msg)
|
||||
);
|
||||
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
const blobBuffer = await convertedBlob.arrayBuffer();
|
||||
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'pdfa-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
pdfFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdfA);
|
||||
}
|
||||
});
|
||||
201
src/js/logic/pdf-to-svg-page.ts
Normal file
201
src/js/logic/pdf-to-svg-page.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please upload at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading Engine...');
|
||||
|
||||
try {
|
||||
await pymupdf.load();
|
||||
|
||||
const isSingleFile = files.length === 1;
|
||||
|
||||
if (isSingleFile) {
|
||||
const doc = await pymupdf.open(files[0]);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = files[0].name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
if (pageCount === 1) {
|
||||
showLoader('Converting to SVG...');
|
||||
const page = doc.getPage(0);
|
||||
const svgContent = page.toSvg();
|
||||
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
downloadFile(svgBlob, `${baseName}.svg`);
|
||||
showAlert('Success', 'PDF converted to SVG successfully!', 'success', () => resetState());
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
|
||||
const page = doc.getPage(i);
|
||||
const svgContent = page.toSvg();
|
||||
zip.file(`page_${i + 1}.svg`, svgContent);
|
||||
}
|
||||
showLoader('Creating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
||||
showAlert('Success', `Converted ${pageCount} pages to SVG!`, 'success', () => resetState());
|
||||
}
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
let totalPages = 0;
|
||||
|
||||
for (let f = 0; f < files.length; f++) {
|
||||
const file = files[f];
|
||||
showLoader(`Processing file ${f + 1} of ${files.length}...`);
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`);
|
||||
const page = doc.getPage(i);
|
||||
const svgContent = page.toSvg();
|
||||
const fileName = pageCount === 1 ? `${baseName}.svg` : `${baseName}_page_${i + 1}.svg`;
|
||||
zip.file(fileName, svgContent);
|
||||
totalPages++;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
||||
showAlert('Success', `Converted ${files.length} files (${totalPages} pages) to SVG!`, 'success', () => resetState());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null, replace = false) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid Files', 'Please upload PDF files.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
files = validFiles;
|
||||
} else {
|
||||
files = [...files, ...validFiles];
|
||||
}
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files, files.length === 0);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null, files.length === 0);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput?.click());
|
||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||
if (processBtn) processBtn.addEventListener('click', convert);
|
||||
});
|
||||
211
src/js/logic/pdf-to-text-page.ts
Normal file
211
src/js/logic/pdf-to-text-page.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDF | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-600');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-600');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-600');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
handleFiles(droppedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extractText);
|
||||
}
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
handleFiles(input.files);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
const validFiles = Array.from(newFiles).filter(file =>
|
||||
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only PDF files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const extractOptions = document.getElementById('extract-options');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !extractOptions) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
extractOptions.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
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 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
extractOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
||||
if (!pymupdf) {
|
||||
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
}
|
||||
return pymupdf;
|
||||
}
|
||||
|
||||
async function extractText() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
|
||||
try {
|
||||
const mupdf = await ensurePyMuPDF();
|
||||
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
showLoader(`Extracting text from ${file.name}...`);
|
||||
|
||||
const fullText = await mupdf.pdfToText(file);
|
||||
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
const textBlob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
|
||||
downloadFile(textBlob, `${baseName}.txt`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Success', 'Text extracted successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} else {
|
||||
showLoader('Extracting text from multiple files...');
|
||||
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
showLoader(`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`);
|
||||
|
||||
const fullText = await mupdf.pdfToText(file);
|
||||
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
zip.file(`${baseName}.txt`, fullText);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdf-to-text.zip');
|
||||
|
||||
hideLoader();
|
||||
showAlert('Success', `Extracted text from ${files.length} PDF files!`, 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[PDFToText]', e);
|
||||
hideLoader();
|
||||
showAlert('Extraction Error', e.message || 'Failed to extract text from PDF.');
|
||||
}
|
||||
}
|
||||
218
src/js/logic/powerpoint-to-pdf-page.ts
Normal file
218
src/js/logic/powerpoint-to-pdf-page.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PowerPoint file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.(ppt|pptx|odp)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.(ppt|pptx|odp)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'powerpoint-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PowerPoint file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pptFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return name.endsWith('.ppt') || name.endsWith('.pptx') || name.endsWith('.odp');
|
||||
});
|
||||
if (pptFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
pptFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
203
src/js/logic/prepare-pdf-for-ai-page.ts
Normal file
203
src/js/logic/prepare-pdf-for-ai-page.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const extractOptions = document.getElementById('extract-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
extractOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
extractOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const extractForAI = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PyMuPDF...');
|
||||
await pymupdf.load();
|
||||
|
||||
const total = state.files.length;
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (total === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Extracting ${file.name} for AI...`);
|
||||
|
||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||
downloadFile(new Blob([jsonContent], { type: 'application/json' }), outName);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Extraction Complete', `Successfully extracted PDF for AI/LLM use.`, 'success', () => resetState());
|
||||
} else {
|
||||
// Multiple files - create ZIP
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const file of state.files) {
|
||||
try {
|
||||
showLoader(`Extracting ${file.name} for AI (${completed + 1}/${total})...`);
|
||||
|
||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||
zip.file(outName, jsonContent);
|
||||
|
||||
completed++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to extract ${file.name}:`, error);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'pdf-for-ai.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (failed === 0) {
|
||||
showAlert('Extraction Complete', `Successfully extracted ${completed} PDF(s) for AI/LLM use.`, 'success', () => resetState());
|
||||
} else {
|
||||
showAlert('Extraction Partial', `Extracted ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length > 0) {
|
||||
state.files = [...state.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extractForAI);
|
||||
}
|
||||
});
|
||||
133
src/js/logic/psd-to-pdf-page.ts
Normal file
133
src/js/logic/psd-to-pdf-page.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.psd'];
|
||||
const FILETYPE_NAME = 'PSD';
|
||||
|
||||
let pymupdf: PyMuPDF | null = null;
|
||||
|
||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
||||
if (!pymupdf) {
|
||||
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
}
|
||||
return pymupdf;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showLoader('Loading PyMuPDF engine...');
|
||||
const mupdf = await ensurePyMuPDF();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await mupdf.imageToPdf(file, { imageType: 'psd' });
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const pdfBlob = await mupdf.imagesToPdf(state.files);
|
||||
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} PSD files to a single PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||
if (processBtn) processBtn.addEventListener('click', convert);
|
||||
});
|
||||
142
src/js/logic/pub-to-pdf-page.ts
Normal file
142
src/js/logic/pub-to-pdf-page.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.pub'];
|
||||
const FILETYPE_NAME = 'PUB';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||
if (processBtn) processBtn.addEventListener('click', convert);
|
||||
|
||||
updateUI();
|
||||
});
|
||||
218
src/js/logic/rasterize-pdf-page.ts
Normal file
218
src/js/logic/rasterize-pdf-page.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const rasterizeOptions = document.getElementById('rasterize-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
rasterizeOptions.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
rasterizeOptions.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const rasterize = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PyMuPDF...');
|
||||
await pymupdf.load();
|
||||
|
||||
// Get options from UI
|
||||
const dpi = parseInt((document.getElementById('rasterize-dpi') as HTMLSelectElement).value) || 150;
|
||||
const format = (document.getElementById('rasterize-format') as HTMLSelectElement).value as 'png' | 'jpeg';
|
||||
const grayscale = (document.getElementById('rasterize-grayscale') as HTMLInputElement).checked;
|
||||
|
||||
const total = state.files.length;
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (total === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Rasterizing ${file.name}...`);
|
||||
|
||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
||||
dpi,
|
||||
format,
|
||||
grayscale,
|
||||
quality: 95
|
||||
});
|
||||
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||
downloadFile(rasterizedBlob, outName);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Rasterization Complete', `Successfully rasterized PDF at ${dpi} DPI.`, 'success', () => resetState());
|
||||
} else {
|
||||
// Multiple files - create ZIP
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const file of state.files) {
|
||||
try {
|
||||
showLoader(`Rasterizing ${file.name} (${completed + 1}/${total})...`);
|
||||
|
||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
||||
dpi,
|
||||
format,
|
||||
grayscale,
|
||||
quality: 95
|
||||
});
|
||||
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||
zip.file(outName, rasterizedBlob);
|
||||
|
||||
completed++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to rasterize ${file.name}:`, error);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'rasterized-pdfs.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (failed === 0) {
|
||||
showAlert('Rasterization Complete', `Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`, 'success', () => resetState());
|
||||
} else {
|
||||
showAlert('Rasterization Partial', `Rasterized ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during rasterization. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length > 0) {
|
||||
state.files = [...state.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', rasterize);
|
||||
}
|
||||
});
|
||||
386
src/js/logic/rotate-custom-page.ts
Normal file
386
src/js/logic/rotate-custom-page.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib';
|
||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface RotateState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
||||
rotations: number[];
|
||||
}
|
||||
|
||||
const pageState: RotateState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
pdfJsDoc: null,
|
||||
rotations: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
cleanupLazyRendering();
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
pageState.pdfJsDoc = null;
|
||||
pageState.rotations = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const pageThumbnails = document.getElementById('page-thumbnails');
|
||||
if (pageThumbnails) pageThumbnails.innerHTML = '';
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
||||
if (batchAngle) batchAngle.value = '0';
|
||||
}
|
||||
|
||||
function updateAllRotationDisplays() {
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
|
||||
if (input) input.value = pageState.rotations[i].toString();
|
||||
const container = document.querySelector(`[data-page-index="${i}"]`);
|
||||
if (container) {
|
||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLElement {
|
||||
const pageIndex = pageNumber - 1;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
|
||||
container.dataset.pageIndex = pageIndex.toString();
|
||||
container.dataset.pageNumber = pageNumber.toString();
|
||||
|
||||
const canvasWrapper = document.createElement('div');
|
||||
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
|
||||
canvasWrapper.style.transition = 'transform 0.3s ease';
|
||||
// Apply initial rotation if it exists (negated for canvas display)
|
||||
const initialRotation = pageState.rotations[pageIndex] || 0;
|
||||
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
|
||||
|
||||
canvas.className = 'max-w-full max-h-full object-contain';
|
||||
canvasWrapper.appendChild(canvas);
|
||||
|
||||
const pageLabel = document.createElement('div');
|
||||
pageLabel.className = 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
|
||||
pageLabel.textContent = `${pageNumber}`;
|
||||
|
||||
container.appendChild(canvasWrapper);
|
||||
container.appendChild(pageLabel);
|
||||
|
||||
// Per-page rotation controls - Custom angle input
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
|
||||
|
||||
const decrementBtn = document.createElement('button');
|
||||
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
||||
decrementBtn.textContent = '-';
|
||||
decrementBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const current = parseInt(input.value) || 0;
|
||||
input.value = (current - 1).toString();
|
||||
};
|
||||
|
||||
const angleInput = document.createElement('input');
|
||||
angleInput.type = 'number';
|
||||
angleInput.id = `page-angle-${pageIndex}`;
|
||||
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
|
||||
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
|
||||
|
||||
const incrementBtn = document.createElement('button');
|
||||
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
||||
incrementBtn.textContent = '+';
|
||||
incrementBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const current = parseInt(input.value) || 0;
|
||||
input.value = (current + 1).toString();
|
||||
};
|
||||
|
||||
const applyBtn = document.createElement('button');
|
||||
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
|
||||
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
|
||||
applyBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const angle = parseInt(input.value) || 0;
|
||||
pageState.rotations[pageIndex] = angle;
|
||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
|
||||
};
|
||||
|
||||
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
|
||||
container.appendChild(controls);
|
||||
|
||||
// Re-create icons for the new element
|
||||
setTimeout(function () {
|
||||
createIcons({ icons });
|
||||
}, 0);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
async function renderThumbnails() {
|
||||
const pageThumbnails = document.getElementById('page-thumbnails');
|
||||
if (!pageThumbnails || !pageState.pdfJsDoc) return;
|
||||
|
||||
pageThumbnails.innerHTML = '';
|
||||
|
||||
await renderPagesProgressively(
|
||||
pageState.pdfJsDoc,
|
||||
pageThumbnails,
|
||||
createPageWrapper,
|
||||
{
|
||||
batchSize: 8,
|
||||
useLazyLoading: true,
|
||||
lazyLoadMargin: '200px',
|
||||
eagerLoadBatches: 2,
|
||||
onBatchComplete: function () {
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
|
||||
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise;
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
pageState.rotations = new Array(pageCount).fill(0);
|
||||
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
await renderThumbnails();
|
||||
hideLoader();
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRotations() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Applying rotations...');
|
||||
|
||||
try {
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const rotation = pageState.rotations[i] || 0;
|
||||
const originalPage = pageState.pdfDoc.getPage(i);
|
||||
const currentRotation = originalPage.getRotation().angle;
|
||||
const totalRotation = currentRotation + rotation;
|
||||
|
||||
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
|
||||
|
||||
if (totalRotation % 90 === 0) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
copiedPage.setRotation(degrees(totalRotation));
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
} else {
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
const { width, height } = embeddedPage.scale(1);
|
||||
|
||||
const angleRad = (totalRotation * Math.PI) / 180;
|
||||
const absCos = Math.abs(Math.cos(angleRad));
|
||||
const absSin = Math.abs(Math.sin(angleRad));
|
||||
|
||||
const newWidth = width * absCos + height * absSin;
|
||||
const newHeight = width * absSin + height * absCos;
|
||||
|
||||
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
|
||||
|
||||
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
|
||||
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
|
||||
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotate: degrees(totalRotation),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rotatedPdfBytes = await newPdfDoc.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_rotated.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Rotations applied successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not apply rotations.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const batchDecrement = document.getElementById('batch-decrement');
|
||||
const batchIncrement = document.getElementById('batch-increment');
|
||||
const batchApply = document.getElementById('batch-apply');
|
||||
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (batchDecrement && batchAngleInput) {
|
||||
batchDecrement.addEventListener('click', function () {
|
||||
const current = parseInt(batchAngleInput.value) || 0;
|
||||
batchAngleInput.value = (current - 1).toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (batchIncrement && batchAngleInput) {
|
||||
batchIncrement.addEventListener('click', function () {
|
||||
const current = parseInt(batchAngleInput.value) || 0;
|
||||
batchAngleInput.value = (current + 1).toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (batchApply && batchAngleInput) {
|
||||
batchApply.addEventListener('click', function () {
|
||||
const angle = parseInt(batchAngleInput.value) || 0;
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
pageState.rotations[i] = angle;
|
||||
}
|
||||
updateAllRotationDisplays();
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', applyRotations);
|
||||
}
|
||||
});
|
||||
@@ -39,19 +39,14 @@ function resetState() {
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
||||
if (batchAngle) batchAngle.value = '0';
|
||||
}
|
||||
|
||||
function updateAllRotationDisplays() {
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
|
||||
if (input) input.value = pageState.rotations[i].toString();
|
||||
const container = document.querySelector(`[data-page-index="${i}"]`);
|
||||
if (container) {
|
||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[i]}deg)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +62,9 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
|
||||
const canvasWrapper = document.createElement('div');
|
||||
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
|
||||
canvasWrapper.style.transition = 'transform 0.3s ease';
|
||||
// Apply initial rotation if it exists
|
||||
const initialRotation = pageState.rotations[pageIndex] || 0;
|
||||
canvasWrapper.style.transform = `rotate(${initialRotation}deg)`;
|
||||
|
||||
canvas.className = 'max-w-full max-h-full object-contain';
|
||||
canvasWrapper.appendChild(canvas);
|
||||
@@ -78,49 +76,31 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
|
||||
container.appendChild(canvasWrapper);
|
||||
container.appendChild(pageLabel);
|
||||
|
||||
// Per-page rotation controls
|
||||
// Per-page rotation controls - Left and Right buttons only
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
|
||||
controls.className = 'flex items-center justify-center gap-2 p-2 bg-gray-800';
|
||||
|
||||
const decrementBtn = document.createElement('button');
|
||||
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
||||
decrementBtn.textContent = '−';
|
||||
decrementBtn.onclick = function (e) {
|
||||
const rotateLeftBtn = document.createElement('button');
|
||||
rotateLeftBtn.className = 'flex items-center gap-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-xs';
|
||||
rotateLeftBtn.innerHTML = '<i data-lucide="rotate-ccw" class="w-3 h-3"></i>';
|
||||
rotateLeftBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const current = parseInt(input.value) || 0;
|
||||
input.value = (current - 1).toString();
|
||||
};
|
||||
|
||||
const angleInput = document.createElement('input');
|
||||
angleInput.type = 'number';
|
||||
angleInput.id = `page-angle-${pageIndex}`;
|
||||
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
|
||||
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
|
||||
|
||||
const incrementBtn = document.createElement('button');
|
||||
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
||||
incrementBtn.textContent = '+';
|
||||
incrementBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const current = parseInt(input.value) || 0;
|
||||
input.value = (current + 1).toString();
|
||||
};
|
||||
|
||||
const applyBtn = document.createElement('button');
|
||||
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
|
||||
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
|
||||
applyBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
||||
const angle = parseInt(input.value) || 0;
|
||||
pageState.rotations[pageIndex] = angle;
|
||||
pageState.rotations[pageIndex] = pageState.rotations[pageIndex] - 90;
|
||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[pageIndex]}deg)`;
|
||||
};
|
||||
|
||||
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
|
||||
const rotateRightBtn = document.createElement('button');
|
||||
rotateRightBtn.className = 'flex items-center gap-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-xs';
|
||||
rotateRightBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-3 h-3"></i>';
|
||||
rotateRightBtn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
pageState.rotations[pageIndex] = pageState.rotations[pageIndex] + 90;
|
||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
||||
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[pageIndex]}deg)`;
|
||||
};
|
||||
|
||||
controls.append(rotateLeftBtn, rotateRightBtn);
|
||||
container.appendChild(controls);
|
||||
|
||||
// Re-create icons for the new element
|
||||
@@ -240,6 +220,8 @@ async function applyRotations() {
|
||||
const currentRotation = originalPage.getRotation().angle;
|
||||
const totalRotation = currentRotation + rotation;
|
||||
|
||||
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
|
||||
|
||||
if (totalRotation % 90 === 0) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
copiedPage.setRotation(degrees(totalRotation));
|
||||
@@ -306,10 +288,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const rotateAllLeft = document.getElementById('rotate-all-left');
|
||||
const rotateAllRight = document.getElementById('rotate-all-right');
|
||||
const batchDecrement = document.getElementById('batch-decrement');
|
||||
const batchIncrement = document.getElementById('batch-increment');
|
||||
const batchApply = document.getElementById('batch-apply');
|
||||
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
@@ -320,7 +298,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (rotateAllLeft) {
|
||||
rotateAllLeft.addEventListener('click', function () {
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
pageState.rotations[i] = pageState.rotations[i] + 90;
|
||||
pageState.rotations[i] = pageState.rotations[i] - 90;
|
||||
}
|
||||
updateAllRotationDisplays();
|
||||
});
|
||||
@@ -329,38 +307,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (rotateAllRight) {
|
||||
rotateAllRight.addEventListener('click', function () {
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
pageState.rotations[i] = pageState.rotations[i] - 90;
|
||||
pageState.rotations[i] = pageState.rotations[i] + 90;
|
||||
}
|
||||
updateAllRotationDisplays();
|
||||
});
|
||||
}
|
||||
|
||||
if (batchDecrement && batchAngleInput) {
|
||||
batchDecrement.addEventListener('click', function () {
|
||||
const current = parseInt(batchAngleInput.value) || 0;
|
||||
batchAngleInput.value = (current - 1).toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (batchIncrement && batchAngleInput) {
|
||||
batchIncrement.addEventListener('click', function () {
|
||||
const current = parseInt(batchAngleInput.value) || 0;
|
||||
batchAngleInput.value = (current + 1).toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (batchApply && batchAngleInput) {
|
||||
batchApply.addEventListener('click', function () {
|
||||
const angle = parseInt(batchAngleInput.value) || 0;
|
||||
if (angle !== 0) {
|
||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||
pageState.rotations[i] = pageState.rotations[i] + angle;
|
||||
}
|
||||
updateAllRotationDisplays();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
|
||||
215
src/js/logic/rtf-to-pdf-page.ts
Normal file
215
src/js/logic/rtf-to-pdf-page.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one RTF file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.rtf$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple RTF files to PDF...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.rtf$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'rtf-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} RTF file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const rtfFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.rtf') || f.type === 'text/rtf' || f.type === 'application/rtf');
|
||||
if (rtfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
rtfFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
@@ -1,25 +1,17 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
|
||||
import { getFontForLanguage, getLanguageForChar } from '../utils/font-loader.js';
|
||||
import { languageToFontFamily } from '../config/font-mappings.js';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
let files: File[] = [];
|
||||
let currentMode: 'upload' | 'text' = 'upload';
|
||||
let selectedLanguages: string[] = ['eng'];
|
||||
|
||||
const allLanguages = Object.keys(languageToFontFamily).sort().map(code => {
|
||||
let name = code;
|
||||
try {
|
||||
const displayNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
||||
name = displayNames.of(code) || code;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to get language name for ${code}`, e);
|
||||
}
|
||||
return { code, name: `${name} (${code})` };
|
||||
});
|
||||
// RTL character detection pattern (Arabic, Hebrew, Persian, etc.)
|
||||
const RTL_PATTERN = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u08A0-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
||||
|
||||
function hasRtlCharacters(text: string): boolean {
|
||||
return RTL_PATTERN.test(text);
|
||||
}
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
@@ -73,140 +65,11 @@ const resetState = () => {
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function createPdfFromText(
|
||||
text: string,
|
||||
fontSize: number,
|
||||
pageSizeKey: string,
|
||||
colorHex: string,
|
||||
orientation: string,
|
||||
customWidth?: number,
|
||||
customHeight?: number
|
||||
): Promise<Uint8Array> {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
const fontMap = new Map<string, any>();
|
||||
const fallbackFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
if (!selectedLanguages.includes('eng')) {
|
||||
selectedLanguages.push('eng');
|
||||
}
|
||||
|
||||
for (const lang of selectedLanguages) {
|
||||
try {
|
||||
const fontBytes = await getFontForLanguage(lang);
|
||||
const font = await pdfDoc.embedFont(fontBytes, { subset: false });
|
||||
fontMap.set(lang, font);
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load font for ${lang}, using fallback`, e);
|
||||
fontMap.set(lang, fallbackFont);
|
||||
}
|
||||
}
|
||||
|
||||
let pageSize = pageSizeKey === 'Custom'
|
||||
? [customWidth || 595, customHeight || 842] as [number, number]
|
||||
: (PageSizes as any)[pageSizeKey];
|
||||
|
||||
if (orientation === 'landscape') {
|
||||
pageSize = [pageSize[1], pageSize[0]] as [number, number];
|
||||
}
|
||||
|
||||
const margin = 72;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
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) {
|
||||
if (paragraph.trim() === '') {
|
||||
y -= lineHeight;
|
||||
if (y < margin) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const words = paragraph.split(' ');
|
||||
let currentLineWords: { text: string; font: any }[] = [];
|
||||
let currentLineWidth = 0;
|
||||
|
||||
for (const word of words) {
|
||||
let wordLang = 'eng';
|
||||
|
||||
for (const char of word) {
|
||||
const charLang = getLanguageForChar(char);
|
||||
if (selectedLanguages.includes(charLang)) {
|
||||
wordLang = charLang;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const font = fontMap.get(wordLang) || fontMap.get('eng') || fallbackFont;
|
||||
const wordWidth = font.widthOfTextAtSize(word + ' ', fontSize);
|
||||
|
||||
if (currentLineWidth + wordWidth > textWidth && currentLineWords.length > 0) {
|
||||
currentLineWords.forEach((item, idx) => {
|
||||
const x = margin + (currentLineWidth * idx / currentLineWords.length);
|
||||
page.drawText(item.text, {
|
||||
x: margin + (currentLineWidth - textWidth) / 2,
|
||||
y: y,
|
||||
size: fontSize,
|
||||
font: item.font,
|
||||
color: rgb(textColor.r / 255, textColor.g / 255, textColor.b / 255),
|
||||
});
|
||||
});
|
||||
|
||||
currentLineWords = [];
|
||||
currentLineWidth = 0;
|
||||
y -= lineHeight;
|
||||
|
||||
if (y < margin) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
}
|
||||
|
||||
currentLineWords.push({ text: word + ' ', font });
|
||||
currentLineWidth += wordWidth;
|
||||
}
|
||||
|
||||
if (currentLineWords.length > 0) {
|
||||
let x = margin;
|
||||
currentLineWords.forEach((item) => {
|
||||
page.drawText(item.text, {
|
||||
x: x,
|
||||
y: y,
|
||||
size: fontSize,
|
||||
font: item.font,
|
||||
color: rgb(textColor.r / 255, textColor.g / 255, textColor.b / 255),
|
||||
});
|
||||
x += item.font.widthOfTextAtSize(item.text, fontSize);
|
||||
});
|
||||
|
||||
y -= lineHeight;
|
||||
if (y < margin) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await pdfDoc.save();
|
||||
}
|
||||
|
||||
async function convert() {
|
||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 12;
|
||||
const pageSizeKey = (document.getElementById('page-size') as HTMLSelectElement).value;
|
||||
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
|
||||
const orientation = (document.getElementById('page-orientation') as HTMLSelectElement).value;
|
||||
const customWidth = parseInt((document.getElementById('custom-width') as HTMLInputElement)?.value);
|
||||
const customHeight = parseInt((document.getElementById('custom-height') as HTMLInputElement)?.value);
|
||||
const fontName = (document.getElementById('font-family') as HTMLSelectElement)?.value || 'helv';
|
||||
const textColor = (document.getElementById('text-color') as HTMLInputElement)?.value || '#000000';
|
||||
|
||||
if (currentMode === 'upload' && files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one text file.');
|
||||
@@ -221,58 +84,59 @@ async function convert() {
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Creating PDF...');
|
||||
showLoader('Loading engine...');
|
||||
|
||||
try {
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
let textContent = '';
|
||||
|
||||
if (currentMode === 'upload') {
|
||||
let combinedText = '';
|
||||
for (const file of files) {
|
||||
const text = await file.text();
|
||||
combinedText += text + '\n\n';
|
||||
textContent += text + '\n\n';
|
||||
}
|
||||
|
||||
const pdfBytes = await createPdfFromText(
|
||||
combinedText,
|
||||
fontSize,
|
||||
pageSizeKey,
|
||||
colorHex,
|
||||
orientation,
|
||||
customWidth,
|
||||
customHeight
|
||||
);
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_text.pdf'
|
||||
);
|
||||
} else {
|
||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
||||
const pdfBytes = await createPdfFromText(
|
||||
textInput.value,
|
||||
fontSize,
|
||||
pageSizeKey,
|
||||
colorHex,
|
||||
orientation,
|
||||
customWidth,
|
||||
customHeight
|
||||
);
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_text.pdf'
|
||||
);
|
||||
textContent = textInput.value;
|
||||
}
|
||||
|
||||
showLoader('Creating PDF...');
|
||||
|
||||
const pdfBlob = await pymupdf.textToPdf(textContent, {
|
||||
fontSize,
|
||||
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
|
||||
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
|
||||
textColor,
|
||||
margins: 72
|
||||
});
|
||||
|
||||
downloadFile(pdfBlob, 'text_to_pdf.pdf');
|
||||
|
||||
showAlert('Success', 'Text converted to PDF successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert text to PDF.');
|
||||
} catch (e: any) {
|
||||
console.error('[TxtToPDF] Error:', e);
|
||||
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// Update textarea direction based on RTL detection
|
||||
function updateTextareaDirection(textarea: HTMLTextAreaElement) {
|
||||
const text = textarea.value;
|
||||
if (hasRtlCharacters(text)) {
|
||||
textarea.style.direction = 'rtl';
|
||||
textarea.style.textAlign = 'right';
|
||||
} else {
|
||||
textarea.style.direction = 'ltr';
|
||||
textarea.style.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
@@ -284,12 +148,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const textModeBtn = document.getElementById('txt-mode-text-btn');
|
||||
const uploadPanel = document.getElementById('txt-upload-panel');
|
||||
const textPanel = document.getElementById('txt-text-panel');
|
||||
const pageSizeSelect = document.getElementById('page-size') as HTMLSelectElement;
|
||||
const customSizeContainer = document.getElementById('custom-size-container');
|
||||
const langDropdownBtn = document.getElementById('lang-dropdown-btn');
|
||||
const langDropdownContent = document.getElementById('lang-dropdown-content');
|
||||
const langSearch = document.getElementById('lang-search') as HTMLInputElement;
|
||||
const langContainer = document.getElementById('language-list-container');
|
||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
||||
|
||||
// Back to Tools
|
||||
if (backBtn) {
|
||||
@@ -321,86 +180,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Custom page size toggle
|
||||
if (pageSizeSelect && customSizeContainer) {
|
||||
pageSizeSelect.addEventListener('change', () => {
|
||||
if (pageSizeSelect.value === 'Custom') {
|
||||
customSizeContainer.classList.remove('hidden');
|
||||
} else {
|
||||
customSizeContainer.classList.add('hidden');
|
||||
}
|
||||
// RTL auto-detection for textarea
|
||||
if (textInput) {
|
||||
textInput.addEventListener('input', () => {
|
||||
updateTextareaDirection(textInput);
|
||||
});
|
||||
}
|
||||
|
||||
// Language dropdown
|
||||
if (langDropdownBtn && langDropdownContent && langContainer) {
|
||||
// Populate language list
|
||||
allLanguages.forEach(lang => {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center gap-2 p-2 hover:bg-gray-700 rounded cursor-pointer';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = lang.code;
|
||||
checkbox.className = 'w-4 h-4';
|
||||
checkbox.checked = lang.code === 'eng';
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
if (!selectedLanguages.includes(lang.code)) {
|
||||
selectedLanguages.push(lang.code);
|
||||
}
|
||||
} else {
|
||||
selectedLanguages = selectedLanguages.filter(l => l !== lang.code);
|
||||
}
|
||||
updateLanguageDisplay();
|
||||
});
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = lang.name;
|
||||
span.className = 'text-sm text-gray-300';
|
||||
|
||||
label.append(checkbox, span);
|
||||
langContainer.appendChild(label);
|
||||
});
|
||||
|
||||
langDropdownBtn.addEventListener('click', () => {
|
||||
langDropdownContent.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!langDropdownBtn.contains(e.target as Node) && !langDropdownContent.contains(e.target as Node)) {
|
||||
langDropdownContent.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
if (langSearch) {
|
||||
langSearch.addEventListener('input', () => {
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
const labels = langContainer.querySelectorAll('label');
|
||||
labels.forEach(label => {
|
||||
const text = label.textContent?.toLowerCase() || '';
|
||||
if (text.includes(searchTerm)) {
|
||||
(label as HTMLElement).style.display = 'flex';
|
||||
} else {
|
||||
(label as HTMLElement).style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateLanguageDisplay() {
|
||||
const langDropdownText = document.getElementById('lang-dropdown-text');
|
||||
if (langDropdownText) {
|
||||
const selectedNames = selectedLanguages.map(code => {
|
||||
const lang = allLanguages.find(l => l.code === code);
|
||||
return lang?.name || code;
|
||||
});
|
||||
langDropdownText.textContent = selectedNames.length > 0 ? selectedNames.join(', ') : 'Select Languages';
|
||||
}
|
||||
}
|
||||
|
||||
// File handling
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
|
||||
142
src/js/logic/vsd-to-pdf-page.ts
Normal file
142
src/js/logic/vsd-to-pdf-page.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.vsd', '.vsdx'];
|
||||
const FILETYPE_NAME = 'VSD';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||
if (processBtn) processBtn.addEventListener('click', convert);
|
||||
|
||||
updateUI();
|
||||
});
|
||||
236
src/js/logic/word-to-pdf-page.ts
Normal file
236
src/js/logic/word-to-pdf-page.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
console.log('[Word2PDF] Starting conversion...');
|
||||
console.log('[Word2PDF] Number of files:', state.files.length);
|
||||
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one Word document.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
console.log('[Word2PDF] Got converter instance');
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
console.log('[Word2PDF] Initializing LibreOffice...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
console.log('[Word2PDF] Init progress:', progress.percent + '%', progress.message);
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
console.log('[Word2PDF] LibreOffice initialized successfully!');
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
console.log('[Word2PDF] Converting single file:', originalFile.name);
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
console.log('[Word2PDF] Conversion complete! PDF size:', pdfBlob.size);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.(doc|docx|odt|rtf)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
console.log('[Word2PDF] File downloaded:', fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
console.log('[Word2PDF] Converting multiple files:', state.files.length);
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
console.log(`[Word2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name);
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
console.log(`[Word2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size);
|
||||
|
||||
const baseName = file.name.replace(/\.(doc|docx|odt|rtf)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
console.log('[Word2PDF] Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
console.log('[Word2PDF] ZIP size:', zipBlob.size);
|
||||
|
||||
downloadFile(zipBlob, 'word-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} Word document(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[Word2PDF] ERROR:', e);
|
||||
console.error('[Word2PDF] Error stack:', e.stack);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const wordFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return name.endsWith('.doc') || name.endsWith('.docx') || name.endsWith('.odt') || name.endsWith('.rtf');
|
||||
});
|
||||
if (wordFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
wordFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
// Initialize UI state (ensures button is hidden when no files)
|
||||
updateUI();
|
||||
});
|
||||
188
src/js/logic/wpd-to-pdf-page.ts
Normal file
188
src/js/logic/wpd-to-pdf-page.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.wpd'];
|
||||
const FILETYPE_NAME = 'WPD';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
188
src/js/logic/wps-to-pdf-page.ts
Normal file
188
src/js/logic/wps-to-pdf-page.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.wps'];
|
||||
const FILETYPE_NAME = 'WPS';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
181
src/js/logic/xml-to-pdf-page.ts
Normal file
181
src/js/logic/xml-to-pdf-page.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { convertXmlToPdf } from '../utils/xml-to-pdf.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.xml'];
|
||||
const FILETYPE_NAME = 'XML';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(message, percent);
|
||||
}
|
||||
});
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(`File ${i + 1}/${state.files.length}: ${message}`, percent);
|
||||
}
|
||||
});
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
201
src/js/logic/xps-to-pdf-page.ts
Normal file
201
src/js/logic/xps-to-pdf-page.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
|
||||
const FILETYPE = 'xps';
|
||||
const EXTENSIONS = ['.xps', '.oxps'];
|
||||
const TOOL_NAME = 'XPS';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
(processBtn as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdf.load();
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
});
|
||||
104
src/js/main.ts
104
src/js/main.ts
@@ -3,11 +3,13 @@ import { dom, switchView, hideAlert, showLoader, hideLoader, showAlert } from '.
|
||||
import { state, resetState } from './state.js';
|
||||
import { ShortcutsManager } from './logic/shortcuts.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import '@phosphor-icons/web/regular';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import '../css/styles.css';
|
||||
import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
|
||||
import { APP_VERSION, injectVersion } from '../version.js';
|
||||
import { initI18n, applyTranslations, rewriteLinks, injectLanguageSwitcher, createLanguageSwitcher, t } from './i18n/index.js';
|
||||
import { startBackgroundPreload } from './utils/wasm-preloader.js';
|
||||
|
||||
const init = async () => {
|
||||
await initI18n();
|
||||
@@ -207,7 +209,7 @@ const init = async () => {
|
||||
'PDF Form Filler': 'tools:pdfFormFiller',
|
||||
'Create PDF Form': 'tools:createPdfForm',
|
||||
'Remove Blank Pages': 'tools:removeBlankPages',
|
||||
'Image to PDF': 'tools:imageToPdf',
|
||||
'Images to PDF': 'tools:imageToPdf',
|
||||
'PNG to PDF': 'tools:pngToPdf',
|
||||
'WebP to PDF': 'tools:webpToPdf',
|
||||
'SVG to PDF': 'tools:svgToPdf',
|
||||
@@ -233,6 +235,7 @@ const init = async () => {
|
||||
'Add Blank Page': 'tools:addBlankPage',
|
||||
'Reverse Pages': 'tools:reversePages',
|
||||
'Rotate PDF': 'tools:rotatePdf',
|
||||
'Rotate by Custom Degrees': 'tools:rotateCustom',
|
||||
'N-Up PDF': 'tools:nUpPdf',
|
||||
'Combine to Single Page': 'tools:combineToSinglePage',
|
||||
'View Metadata': 'tools:viewMetadata',
|
||||
@@ -287,7 +290,12 @@ const init = async () => {
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
||||
icon.setAttribute('data-lucide', tool.icon);
|
||||
|
||||
if (tool.icon.startsWith('ph-')) {
|
||||
icon.className = `ph ${tool.icon} text-4xl mb-3 text-indigo-400`;
|
||||
} else {
|
||||
icon.setAttribute('data-lucide', tool.icon);
|
||||
}
|
||||
|
||||
const toolName = document.createElement('h3');
|
||||
toolName.className = 'font-semibold text-white';
|
||||
@@ -313,20 +321,68 @@ const init = async () => {
|
||||
const searchBar = document.getElementById('search-bar');
|
||||
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
||||
|
||||
const fuzzyMatch = (searchTerm: string, targetText: string): boolean => {
|
||||
if (!searchTerm) return true;
|
||||
const fuzzyMatchWithScore = (searchTerm: string, targetText: string): number => {
|
||||
if (!searchTerm) return 100;
|
||||
|
||||
const search = searchTerm.toLowerCase();
|
||||
const target = targetText.toLowerCase();
|
||||
|
||||
if (target === search) return 100;
|
||||
|
||||
if (target.includes(search)) {
|
||||
if (target.startsWith(search)) return 95;
|
||||
if (target.includes(' ' + search)) return 90;
|
||||
return 85;
|
||||
}
|
||||
|
||||
const words = target.split(/\s+/);
|
||||
const searchWords = search.split(/\s+/);
|
||||
|
||||
let wordBoundaryScore = 0;
|
||||
let matchedWords = 0;
|
||||
|
||||
for (const searchWord of searchWords) {
|
||||
for (const targetWord of words) {
|
||||
if (targetWord.startsWith(searchWord)) {
|
||||
matchedWords++;
|
||||
wordBoundaryScore += 20;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedWords === searchWords.length) {
|
||||
return Math.min(80, wordBoundaryScore);
|
||||
}
|
||||
|
||||
let searchIndex = 0;
|
||||
let targetIndex = 0;
|
||||
let consecutiveMatches = 0;
|
||||
let maxConsecutive = 0;
|
||||
let totalMatches = 0;
|
||||
|
||||
while (searchIndex < searchTerm.length && targetIndex < targetText.length) {
|
||||
if (searchTerm[searchIndex] === targetText[targetIndex]) {
|
||||
while (searchIndex < search.length && targetIndex < target.length) {
|
||||
if (search[searchIndex] === target[targetIndex]) {
|
||||
searchIndex++;
|
||||
totalMatches++;
|
||||
consecutiveMatches++;
|
||||
maxConsecutive = Math.max(maxConsecutive, consecutiveMatches);
|
||||
} else {
|
||||
consecutiveMatches = 0;
|
||||
}
|
||||
targetIndex++;
|
||||
}
|
||||
|
||||
return searchIndex === searchTerm.length;
|
||||
if (searchIndex !== search.length) return 0;
|
||||
const matchRatio = totalMatches / search.length;
|
||||
const consecutiveBonus = (maxConsecutive / search.length) * 20;
|
||||
const lengthPenalty = Math.max(0, (target.length - search.length) / target.length) * 10;
|
||||
|
||||
const score = Math.max(0, Math.min(75,
|
||||
(matchRatio * 50) + consecutiveBonus - lengthPenalty
|
||||
));
|
||||
|
||||
return score;
|
||||
};
|
||||
|
||||
searchBar.addEventListener('input', () => {
|
||||
@@ -334,20 +390,33 @@ const init = async () => {
|
||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||
|
||||
categoryGroups.forEach((group) => {
|
||||
const toolCards = group.querySelectorAll('.tool-card');
|
||||
const toolCards = Array.from(group.querySelectorAll('.tool-card'));
|
||||
|
||||
const scoredCards = toolCards.map((card) => {
|
||||
const toolName = card.querySelector('h3')?.textContent || '';
|
||||
const toolSubtitle = card.querySelector('p')?.textContent || '';
|
||||
|
||||
const nameScore = fuzzyMatchWithScore(searchTerm, toolName);
|
||||
const subtitleScore = fuzzyMatchWithScore(searchTerm, toolSubtitle);
|
||||
|
||||
const score = Math.max(nameScore, subtitleScore) +
|
||||
(nameScore > 0 && subtitleScore > 0 ? 5 : 0);
|
||||
|
||||
return { card, score };
|
||||
});
|
||||
|
||||
scoredCards.sort((a, b) => b.score - a.score);
|
||||
|
||||
let visibleToolsInCategory = 0;
|
||||
const threshold = 10;
|
||||
|
||||
toolCards.forEach((card) => {
|
||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||
const toolSubtitle =
|
||||
card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||
|
||||
const isMatch =
|
||||
fuzzyMatch(searchTerm, toolName) || fuzzyMatch(searchTerm, toolSubtitle);
|
||||
|
||||
scoredCards.forEach(({ card, score }, index) => {
|
||||
const isMatch = score >= threshold;
|
||||
card.classList.toggle('hidden', !isMatch);
|
||||
|
||||
if (isMatch) {
|
||||
visibleToolsInCategory++;
|
||||
(card as HTMLElement).style.order = index.toString();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -403,6 +472,9 @@ const init = async () => {
|
||||
createIcons({ icons });
|
||||
console.log('Please share our tool and share the love!');
|
||||
|
||||
// Start background WASM preloading on all pages
|
||||
startBackgroundPreload();
|
||||
|
||||
|
||||
const githubStarsElements = [
|
||||
document.getElementById('github-stars-desktop'),
|
||||
|
||||
55
src/js/sw-register.ts
Normal file
55
src/js/sw-register.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Service Worker Registration
|
||||
* Registers the service worker to enable offline caching
|
||||
*
|
||||
* Note: Service Worker is disabled in development mode to prevent
|
||||
* conflicts with Vite's HMR (Hot Module Replacement)
|
||||
*/
|
||||
|
||||
// Skip service worker registration in development mode
|
||||
const isDevelopment = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.port !== '';
|
||||
|
||||
if (isDevelopment) {
|
||||
console.log('[Dev Mode] Service Worker registration skipped in development');
|
||||
console.log('Service Worker will be active in production builds');
|
||||
} else if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
const swPath = `${import.meta.env.BASE_URL}sw.js`;
|
||||
console.log('[SW] Registering Service Worker at:', swPath);
|
||||
navigator.serviceWorker
|
||||
.register(swPath)
|
||||
.then((registration) => {
|
||||
console.log('[SW] Service Worker registered successfully:', registration.scope);
|
||||
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 24 * 60 * 60 * 1000);
|
||||
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
console.log('[SW] New version available! Reload to update.');
|
||||
|
||||
if (confirm('A new version of BentoPDF is available. Reload to update?')) {
|
||||
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[SW] Service Worker registration failed:', error);
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log('[SW] New service worker activated, reloading...');
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
47
src/js/ui.ts
47
src/js/ui.ts
@@ -44,9 +44,48 @@ export const dom = {
|
||||
warningConfirmBtn: document.getElementById('warning-confirm-btn'),
|
||||
};
|
||||
|
||||
export const showLoader = (text = t('common.loading')) => {
|
||||
export const showLoader = (text = t('common.loading'), progress?: number) => {
|
||||
if (dom.loaderText) dom.loaderText.textContent = text;
|
||||
if (dom.loaderModal) dom.loaderModal.classList.remove('hidden');
|
||||
|
||||
// Add or update progress bar if progress is provided
|
||||
const loaderModal = dom.loaderModal;
|
||||
if (loaderModal) {
|
||||
let progressBar = loaderModal.querySelector('.loader-progress-bar') as HTMLElement;
|
||||
let progressContainer = loaderModal.querySelector('.loader-progress-container') as HTMLElement;
|
||||
|
||||
if (progress !== undefined && progress >= 0) {
|
||||
// Create progress container if it doesn't exist
|
||||
if (!progressContainer) {
|
||||
progressContainer = document.createElement('div');
|
||||
progressContainer.className = 'loader-progress-container w-64 mt-4';
|
||||
progressContainer.innerHTML = `
|
||||
<div class="bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div class="loader-progress-bar bg-indigo-500 h-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="loader-progress-text text-xs text-gray-400 mt-1 text-center">0%</p>
|
||||
`;
|
||||
loaderModal.querySelector('.bg-gray-800')?.appendChild(progressContainer);
|
||||
progressBar = progressContainer.querySelector('.loader-progress-bar') as HTMLElement;
|
||||
}
|
||||
|
||||
// Update progress
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${progress}%`;
|
||||
}
|
||||
const progressText = progressContainer.querySelector('.loader-progress-text');
|
||||
if (progressText) {
|
||||
progressText.textContent = `${Math.round(progress)}%`;
|
||||
}
|
||||
progressContainer.classList.remove('hidden');
|
||||
} else {
|
||||
// Hide progress bar if no progress provided
|
||||
if (progressContainer) {
|
||||
progressContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
loaderModal.classList.remove('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
export const hideLoader = () => {
|
||||
@@ -433,8 +472,8 @@ const createFileInputHTML = (options = {}) => {
|
||||
<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">
|
||||
<i data-lucide="plus"></i> ${t('upload.addMore')}
|
||||
</button>
|
||||
<button id="clear-files-btn" class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> ${t('upload.clearAll')}
|
||||
<button id="clear-files-btn" class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> ${t('upload.clearAll')}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
90
src/js/utils/csv-to-pdf.ts
Normal file
90
src/js/utils/csv-to-pdf.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
export interface CsvToPdfOptions {
|
||||
onProgress?: (percent: number, message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CSV file to PDF using jsPDF and autotable
|
||||
*/
|
||||
export async function convertCsvToPdf(
|
||||
file: File,
|
||||
options?: CsvToPdfOptions
|
||||
): Promise<Blob> {
|
||||
const { onProgress } = options || {};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
onProgress?.(10, 'Reading CSV file...');
|
||||
|
||||
Papa.parse(file, {
|
||||
complete: (results) => {
|
||||
try {
|
||||
onProgress?.(50, 'Generating PDF...');
|
||||
|
||||
const data = results.data as string[][];
|
||||
|
||||
// Filter out empty rows
|
||||
const filteredData = data.filter(row =>
|
||||
row.some(cell => cell && cell.trim() !== '')
|
||||
);
|
||||
|
||||
if (filteredData.length === 0) {
|
||||
reject(new Error('CSV file is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PDF document
|
||||
const doc = new jsPDF({
|
||||
orientation: 'landscape', // Better for wide tables
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
// Extract headers (first row) and data
|
||||
const headers = filteredData[0];
|
||||
const rows = filteredData.slice(1);
|
||||
|
||||
onProgress?.(70, 'Creating table...');
|
||||
|
||||
// Generate table
|
||||
autoTable(doc, {
|
||||
head: [headers],
|
||||
body: rows,
|
||||
startY: 20,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3,
|
||||
overflow: 'linebreak',
|
||||
cellWidth: 'wrap',
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [41, 128, 185], // Nice blue header
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [245, 245, 245], // Light gray for alternate rows
|
||||
},
|
||||
margin: { top: 20, left: 10, right: 10 },
|
||||
theme: 'striped',
|
||||
});
|
||||
|
||||
onProgress?.(90, 'Finalizing PDF...');
|
||||
|
||||
// Get PDF as blob
|
||||
const pdfBlob = doc.output('blob');
|
||||
|
||||
onProgress?.(100, 'Complete!');
|
||||
resolve(pdfBlob);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
reject(new Error(`Failed to parse CSV: ${error.message}`));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
210
src/js/utils/ghostscript-loader.ts
Normal file
210
src/js/utils/ghostscript-loader.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* PDF/A Conversion using Ghostscript WASM
|
||||
*
|
||||
* Converts PDFs to PDF/A-1b, PDF/A-2b, or PDF/A-3b format.
|
||||
*/
|
||||
|
||||
import loadWASM from '@bentopdf/gs-wasm';
|
||||
|
||||
interface GhostscriptModule {
|
||||
FS: {
|
||||
writeFile(path: string, data: Uint8Array | string): void;
|
||||
readFile(path: string, opts?: { encoding?: string }): Uint8Array;
|
||||
unlink(path: string): void;
|
||||
stat(path: string): { size: number };
|
||||
};
|
||||
callMain(args: string[]): number;
|
||||
}
|
||||
|
||||
export type PdfALevel = 'PDF/A-1b' | 'PDF/A-2b' | 'PDF/A-3b';
|
||||
|
||||
let cachedGsModule: GhostscriptModule | null = null;
|
||||
|
||||
export function setCachedGsModule(module: GhostscriptModule): void {
|
||||
cachedGsModule = module;
|
||||
}
|
||||
|
||||
export function getCachedGsModule(): GhostscriptModule | null {
|
||||
return cachedGsModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode binary data to Adobe ASCII85 (Base85) format.
|
||||
* This matches Python's base64.a85encode(data, adobe=True)
|
||||
*/
|
||||
function encodeBase85(data: Uint8Array): string {
|
||||
const POW85 = [85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1];
|
||||
let result = '';
|
||||
|
||||
// Process 4 bytes at a time
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Get 4 bytes (pad with zeros if needed)
|
||||
let value = 0;
|
||||
const remaining = Math.min(4, data.length - i);
|
||||
for (let j = 0; j < 4; j++) {
|
||||
value = value * 256 + (j < remaining ? data[i + j] : 0);
|
||||
}
|
||||
|
||||
// Special case: all zeros become 'z'
|
||||
if (value === 0 && remaining === 4) {
|
||||
result += 'z';
|
||||
} else {
|
||||
// Encode to 5 ASCII85 characters
|
||||
const encoded: string[] = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
encoded.push(String.fromCharCode((value / POW85[j]) % 85 + 33));
|
||||
}
|
||||
// For partial blocks, only output needed characters
|
||||
result += encoded.slice(0, remaining + 1).join('');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function convertToPdfA(
|
||||
pdfData: Uint8Array,
|
||||
level: PdfALevel = 'PDF/A-2b',
|
||||
onProgress?: (msg: string) => void
|
||||
): Promise<Uint8Array> {
|
||||
onProgress?.('Loading Ghostscript...');
|
||||
|
||||
let gs: GhostscriptModule;
|
||||
|
||||
if (cachedGsModule) {
|
||||
gs = cachedGsModule;
|
||||
} else {
|
||||
gs = await loadWASM({
|
||||
locateFile: (path: string) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
return import.meta.env.BASE_URL + 'ghostscript-wasm/gs.wasm';
|
||||
}
|
||||
return path;
|
||||
},
|
||||
print: (text: string) => console.log('[GS]', text),
|
||||
printErr: (text: string) => console.error('[GS Error]', text),
|
||||
}) as GhostscriptModule;
|
||||
cachedGsModule = gs;
|
||||
}
|
||||
|
||||
|
||||
const pdfaMap: Record<PdfALevel, string> = {
|
||||
'PDF/A-1b': '1',
|
||||
'PDF/A-2b': '2',
|
||||
'PDF/A-3b': '3',
|
||||
};
|
||||
|
||||
const inputPath = '/tmp/input.pdf';
|
||||
const outputPath = '/tmp/output.pdf';
|
||||
|
||||
gs.FS.writeFile(inputPath, pdfData);
|
||||
console.log('[Ghostscript] Input file size:', pdfData.length);
|
||||
|
||||
onProgress?.(`Converting to ${level}...`);
|
||||
const pdfaDefPath = '/tmp/pdfa.ps';
|
||||
|
||||
try {
|
||||
const response = await fetch(import.meta.env.BASE_URL + 'ghostscript-wasm/sRGB_v4_ICC_preference.icc');
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
|
||||
const iccData = new Uint8Array(await response.arrayBuffer());
|
||||
console.log('[Ghostscript] sRGB v4 ICC profile loaded:', iccData.length, 'bytes');
|
||||
|
||||
// Write ICC profile as a binary file to FS (eliminates encoding issues)
|
||||
const iccPath = '/tmp/pdfa.icc';
|
||||
gs.FS.writeFile(iccPath, iccData);
|
||||
console.log('[Ghostscript] sRGB ICC profile written to FS:', iccPath);
|
||||
|
||||
// Generate PostScript with reference to ICC file (Standard OCRmyPDF/GS approach)
|
||||
const pdfaPS = `%!
|
||||
% Define OutputIntent subtype based on PDF/A level
|
||||
/OutputIntentSubtype ${level === 'PDF/A-1b' ? '/GTS_PDFA1' : '/GTS_PDFA'} def
|
||||
|
||||
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
|
||||
[{icc_PDFA} <</N 3 >> /PUT pdfmark
|
||||
[{icc_PDFA} (${iccPath}) (r) file /PUT pdfmark
|
||||
|
||||
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
|
||||
[{OutputIntent_PDFA} <<
|
||||
/Type /OutputIntent
|
||||
/S OutputIntentSubtype
|
||||
/DestOutputProfile {icc_PDFA}
|
||||
/OutputConditionIdentifier (sRGB)
|
||||
>> /PUT pdfmark
|
||||
|
||||
[{Catalog} <<
|
||||
/OutputIntents [ {OutputIntent_PDFA} ]
|
||||
>> /PUT pdfmark
|
||||
`;
|
||||
gs.FS.writeFile(pdfaDefPath, pdfaPS);
|
||||
console.log('[Ghostscript] PDFA PostScript created with embedded ICC profile');
|
||||
} catch (e) {
|
||||
console.error('[Ghostscript] Failed to create PDFA PostScript:', e);
|
||||
throw new Error('Conversion failed: could not create PDF/A definition');
|
||||
}
|
||||
|
||||
const args = [
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=pdfwrite',
|
||||
`-dPDFA=${pdfaMap[level]}`,
|
||||
'-dPDFACompatibilityPolicy=1',
|
||||
`-dCompatibilityLevel=${level === 'PDF/A-1b' ? '1.4' : '1.7'}`,
|
||||
'-sColorConversionStrategy=RGB',
|
||||
'-dEmbedAllFonts=true',
|
||||
'-dSubsetFonts=true',
|
||||
'-dAutoRotatePages=/None',
|
||||
`-sOutputFile=${outputPath}`,
|
||||
pdfaDefPath,
|
||||
inputPath,
|
||||
];
|
||||
|
||||
console.log('[Ghostscript] Running PDF/A conversion...');
|
||||
|
||||
let exitCode: number;
|
||||
try {
|
||||
exitCode = gs.callMain(args);
|
||||
} catch (e) {
|
||||
console.error('[Ghostscript] Exception:', e);
|
||||
throw new Error(`Ghostscript threw an exception: ${e}`);
|
||||
}
|
||||
|
||||
console.log('[Ghostscript] Exit code:', exitCode);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
try { gs.FS.unlink(inputPath); } catch { /* ignore */ }
|
||||
try { gs.FS.unlink(outputPath); } catch { /* ignore */ }
|
||||
throw new Error(`Ghostscript conversion failed with exit code ${exitCode}`);
|
||||
}
|
||||
|
||||
// Read output
|
||||
let output: Uint8Array;
|
||||
try {
|
||||
const stat = gs.FS.stat(outputPath);
|
||||
console.log('[Ghostscript] Output file size:', stat.size);
|
||||
output = gs.FS.readFile(outputPath);
|
||||
} catch (e) {
|
||||
console.error('[Ghostscript] Failed to read output:', e);
|
||||
throw new Error('Ghostscript did not produce output file');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try { gs.FS.unlink(inputPath); } catch { /* ignore */ }
|
||||
try { gs.FS.unlink(outputPath); } catch { /* ignore */ }
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function convertFileToPdfA(
|
||||
file: File,
|
||||
level: PdfALevel = 'PDF/A-2b',
|
||||
onProgress?: (msg: string) => void
|
||||
): Promise<Blob> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfData = new Uint8Array(arrayBuffer);
|
||||
const result = await convertToPdfA(pdfData, level, onProgress);
|
||||
// Copy to regular ArrayBuffer to avoid SharedArrayBuffer issues
|
||||
const copy = new Uint8Array(result.length);
|
||||
copy.set(result);
|
||||
return new Blob([copy], { type: 'application/pdf' });
|
||||
}
|
||||
158
src/js/utils/libreoffice-loader.ts
Normal file
158
src/js/utils/libreoffice-loader.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* LibreOffice WASM Converter Wrapper
|
||||
*
|
||||
* Uses @matbee/libreoffice-converter package for document conversion.
|
||||
* Handles progress tracking and provides simpler API.
|
||||
*/
|
||||
|
||||
import { WorkerBrowserConverter } from '@matbee/libreoffice-converter/browser';
|
||||
|
||||
export interface LoadProgress {
|
||||
phase: 'loading' | 'initializing' | 'converting' | 'complete' | 'ready';
|
||||
percent: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (progress: LoadProgress) => void;
|
||||
|
||||
// Singleton for converter instance
|
||||
let converterInstance: LibreOfficeConverter | null = null;
|
||||
|
||||
export class LibreOfficeConverter {
|
||||
private converter: WorkerBrowserConverter | null = null;
|
||||
private initialized = false;
|
||||
private initializing = false;
|
||||
private basePath: string;
|
||||
|
||||
constructor(basePath: string = import.meta.env.BASE_URL + 'libreoffice-wasm/') {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
async initialize(onProgress?: ProgressCallback): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
if (this.initializing) {
|
||||
while (this.initializing) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.initializing = true;
|
||||
let progressCallback = onProgress; // Store original callback
|
||||
|
||||
try {
|
||||
progressCallback?.({ phase: 'loading', percent: 0, message: 'Loading conversion engine...' });
|
||||
|
||||
this.converter = new WorkerBrowserConverter({
|
||||
sofficeJs: `${this.basePath}soffice.js`,
|
||||
sofficeWasm: `${this.basePath}soffice.wasm.gz`,
|
||||
sofficeData: `${this.basePath}soffice.data.gz`,
|
||||
sofficeWorkerJs: `${this.basePath}soffice.worker.js`,
|
||||
browserWorkerJs: `${this.basePath}browser.worker.global.js`,
|
||||
verbose: false,
|
||||
onProgress: (info: { phase: string; percent: number; message: string }) => {
|
||||
if (progressCallback && !this.initialized) {
|
||||
const simplifiedMessage = `Loading conversion engine (${Math.round(info.percent)}%)...`;
|
||||
progressCallback({
|
||||
phase: info.phase as LoadProgress['phase'],
|
||||
percent: info.percent,
|
||||
message: simplifiedMessage
|
||||
});
|
||||
}
|
||||
},
|
||||
onReady: () => {
|
||||
console.log('[LibreOffice] Ready!');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('[LibreOffice] Error:', error);
|
||||
},
|
||||
});
|
||||
|
||||
await this.converter.initialize();
|
||||
this.initialized = true;
|
||||
|
||||
// Call completion message
|
||||
progressCallback?.({ phase: 'ready', percent: 100, message: 'Conversion engine ready!' });
|
||||
|
||||
// Null out the callback to prevent any late-firing progress updates
|
||||
progressCallback = undefined;
|
||||
} finally {
|
||||
this.initializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.initialized && this.converter !== null;
|
||||
}
|
||||
|
||||
async convertToPdf(file: File): Promise<Blob> {
|
||||
if (!this.converter) {
|
||||
throw new Error('Converter not initialized');
|
||||
}
|
||||
|
||||
console.log(`[LibreOffice] Converting ${file.name} to PDF...`);
|
||||
console.log(`[LibreOffice] File type: ${file.type}, Size: ${file.size} bytes`);
|
||||
|
||||
try {
|
||||
console.log(`[LibreOffice] Reading file as ArrayBuffer...`);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
console.log(`[LibreOffice] File loaded, ${uint8Array.length} bytes`);
|
||||
|
||||
console.log(`[LibreOffice] Calling converter.convert() with buffer...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Detect input format - critical for CSV to apply import filters
|
||||
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
console.log(`[LibreOffice] Detected format from extension: ${ext}`);
|
||||
|
||||
const result = await this.converter.convert(uint8Array, {
|
||||
outputFormat: 'pdf',
|
||||
inputFormat: ext as any, // Explicitly specify format for CSV import filters
|
||||
}, file.name);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[LibreOffice] Conversion complete! Duration: ${duration}ms, Size: ${result.data.length} bytes`);
|
||||
|
||||
// Create a copy to avoid SharedArrayBuffer type issues
|
||||
const data = new Uint8Array(result.data);
|
||||
return new Blob([data], { type: result.mimeType });
|
||||
} catch (error) {
|
||||
console.error(`[LibreOffice] Conversion FAILED for ${file.name}:`, error);
|
||||
console.error(`[LibreOffice] Error details:`, {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async wordToPdf(file: File): Promise<Blob> {
|
||||
return this.convertToPdf(file);
|
||||
}
|
||||
|
||||
async pptToPdf(file: File): Promise<Blob> {
|
||||
return this.convertToPdf(file);
|
||||
}
|
||||
|
||||
async excelToPdf(file: File): Promise<Blob> {
|
||||
return this.convertToPdf(file);
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
if (this.converter) {
|
||||
await this.converter.destroy();
|
||||
}
|
||||
this.converter = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLibreOfficeConverter(basePath?: string): LibreOfficeConverter {
|
||||
if (!converterInstance) {
|
||||
converterInstance = new LibreOfficeConverter(basePath);
|
||||
}
|
||||
return converterInstance;
|
||||
}
|
||||
797
src/js/utils/markdown-editor.ts
Normal file
797
src/js/utils/markdown-editor.ts
Normal file
@@ -0,0 +1,797 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import python from 'highlight.js/lib/languages/python';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import xml from 'highlight.js/lib/languages/xml';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import bash from 'highlight.js/lib/languages/bash';
|
||||
import markdownLang from 'highlight.js/lib/languages/markdown';
|
||||
import sql from 'highlight.js/lib/languages/sql';
|
||||
import java from 'highlight.js/lib/languages/java';
|
||||
import csharp from 'highlight.js/lib/languages/csharp';
|
||||
import cpp from 'highlight.js/lib/languages/cpp';
|
||||
import go from 'highlight.js/lib/languages/go';
|
||||
import rust from 'highlight.js/lib/languages/rust';
|
||||
import yaml from 'highlight.js/lib/languages/yaml';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import sub from 'markdown-it-sub';
|
||||
import sup from 'markdown-it-sup';
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import deflist from 'markdown-it-deflist';
|
||||
import abbr from 'markdown-it-abbr';
|
||||
import { full as emoji } from 'markdown-it-emoji';
|
||||
import ins from 'markdown-it-ins';
|
||||
import mark from 'markdown-it-mark';
|
||||
import taskLists from 'markdown-it-task-lists';
|
||||
import anchor from 'markdown-it-anchor';
|
||||
import tocDoneRight from 'markdown-it-toc-done-right';
|
||||
import { applyTranslations } from '../i18n/i18n';
|
||||
|
||||
|
||||
|
||||
// Register highlight.js languages
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('js', javascript);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
hljs.registerLanguage('ts', typescript);
|
||||
hljs.registerLanguage('python', python);
|
||||
hljs.registerLanguage('py', python);
|
||||
hljs.registerLanguage('css', css);
|
||||
hljs.registerLanguage('html', xml);
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('json', json);
|
||||
hljs.registerLanguage('bash', bash);
|
||||
hljs.registerLanguage('sh', bash);
|
||||
hljs.registerLanguage('shell', bash);
|
||||
hljs.registerLanguage('markdown', markdownLang);
|
||||
hljs.registerLanguage('md', markdownLang);
|
||||
hljs.registerLanguage('sql', sql);
|
||||
hljs.registerLanguage('java', java);
|
||||
hljs.registerLanguage('csharp', csharp);
|
||||
hljs.registerLanguage('cs', csharp);
|
||||
hljs.registerLanguage('cpp', cpp);
|
||||
hljs.registerLanguage('c', cpp);
|
||||
hljs.registerLanguage('go', go);
|
||||
hljs.registerLanguage('rust', rust);
|
||||
hljs.registerLanguage('yaml', yaml);
|
||||
hljs.registerLanguage('yml', yaml);
|
||||
|
||||
export interface MarkdownEditorOptions {
|
||||
/** Initial markdown content */
|
||||
initialContent?: string;
|
||||
/** Callback when user wants to go back */
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export interface MarkdownItOptions {
|
||||
/** Enable HTML tags in source */
|
||||
html: boolean;
|
||||
/** Convert '\n' in paragraphs into <br> */
|
||||
breaks: boolean;
|
||||
/** Autoconvert URL-like text to links */
|
||||
linkify: boolean;
|
||||
/** Enable some language-neutral replacement + quotes beautification */
|
||||
typographer: boolean;
|
||||
/** Highlight function for fenced code blocks */
|
||||
highlight?: (str: string, lang: string) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor
|
||||
|
||||
This is a **live preview** markdown editor with full plugin support.
|
||||
|
||||
\${toc}
|
||||
|
||||
## Basic Formatting
|
||||
|
||||
- **Bold** and *italic* text
|
||||
- ~~Strikethrough~~ text
|
||||
- [Links](https://bentopdf.com)
|
||||
- ==Highlighted text== using mark
|
||||
- ++Inserted text++ using ins
|
||||
- H~2~O for subscript
|
||||
- E=mc^2^ for superscript
|
||||
|
||||
## Task Lists
|
||||
|
||||
- [x] Completed task
|
||||
- [x] Another done item
|
||||
- [ ] Pending task
|
||||
- [ ] Future work
|
||||
|
||||
## Emoji Support :rocket:
|
||||
|
||||
Use emoji shortcodes: :smile: :heart: :thumbsup: :star: :fire:
|
||||
|
||||
## Code with Syntax Highlighting
|
||||
|
||||
\`\`\`javascript
|
||||
function greet(name) {
|
||||
console.log(\`Hello, \${name}!\`);
|
||||
return { message: 'Welcome!' };
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
\`\`\`python
|
||||
def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
\`\`\`
|
||||
|
||||
## Tables
|
||||
|
||||
| Feature | Supported | Notes |
|
||||
|---------|:---------:|-------|
|
||||
| Headers | ✓ | Multiple levels |
|
||||
| Lists | ✓ | Ordered & unordered |
|
||||
| Code | ✓ | With highlighting |
|
||||
| Tables | ✓ | With alignment |
|
||||
| Emoji | ✓ | :white_check_mark: |
|
||||
|
||||
## Footnotes
|
||||
|
||||
Here's a sentence with a footnote[^1].
|
||||
|
||||
## Definition Lists
|
||||
|
||||
Term 1
|
||||
: Definition for term 1
|
||||
|
||||
Term 2
|
||||
: Definition for term 2
|
||||
: Another definition for term 2
|
||||
|
||||
## Abbreviations
|
||||
|
||||
The HTML specification is maintained by the W3C.
|
||||
|
||||
*[HTML]: Hyper Text Markup Language
|
||||
*[W3C]: World Wide Web Consortium
|
||||
|
||||
---
|
||||
|
||||
Start editing to see the magic happen!
|
||||
|
||||
[^1]: This is the footnote content.
|
||||
`;
|
||||
|
||||
export class MarkdownEditor {
|
||||
private container: HTMLElement;
|
||||
private md: MarkdownIt;
|
||||
private editor: HTMLTextAreaElement | null = null;
|
||||
private preview: HTMLElement | null = null;
|
||||
private onBack?: () => void;
|
||||
private syncScroll: boolean = false;
|
||||
private isSyncing: boolean = false;
|
||||
private mdOptions: MarkdownItOptions = {
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
};
|
||||
|
||||
constructor(container: HTMLElement, options: MarkdownEditorOptions) {
|
||||
this.container = container;
|
||||
this.onBack = options.onBack;
|
||||
|
||||
this.md = this.createMarkdownIt();
|
||||
this.configureLinkRenderer();
|
||||
|
||||
this.render();
|
||||
|
||||
if (options.initialContent) {
|
||||
this.setContent(options.initialContent);
|
||||
} else {
|
||||
this.setContent(DEFAULT_MARKDOWN);
|
||||
}
|
||||
}
|
||||
|
||||
private configureLinkRenderer(): void {
|
||||
// Override link renderer to add target="_blank" and rel="noopener"
|
||||
const defaultRender = this.md.renderer.rules.link_open ||
|
||||
((tokens: any[], idx: number, options: any, _env: any, self: any) => self.renderToken(tokens, idx, options));
|
||||
|
||||
this.md.renderer.rules.link_open = (tokens: any[], idx: number, options: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
token.attrSet('target', '_blank');
|
||||
token.attrSet('rel', 'noopener noreferrer');
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
this.container.innerHTML = `
|
||||
<div class="md-editor light-mode">
|
||||
<div class="md-editor-wrapper">
|
||||
<div class="md-editor-header">
|
||||
<div class="md-editor-actions">
|
||||
<input type="file" accept=".md,.markdown,.txt" id="mdFileInput" style="display: none;" />
|
||||
<button class="md-editor-btn md-editor-btn-secondary" id="mdUpload">
|
||||
<i data-lucide="upload"></i>
|
||||
<span data-i18n="tools:markdownToPdf.btnUpload">Upload</span>
|
||||
</button>
|
||||
<div class="theme-toggle">
|
||||
<i data-lucide="moon" width="16" height="16"></i>
|
||||
<div class="theme-toggle-slider active" id="themeToggle"></div>
|
||||
<i data-lucide="sun" width="16" height="16"></i>
|
||||
</div>
|
||||
<button class="md-editor-btn md-editor-btn-secondary" id="mdSyncScroll" title="Toggle sync scroll">
|
||||
<i data-lucide="git-compare"></i>
|
||||
<span data-i18n="tools:markdownToPdf.btnSyncScroll">Sync Scroll</span>
|
||||
</button>
|
||||
<button class="md-editor-btn md-editor-btn-secondary" id="mdSettings">
|
||||
<i data-lucide="settings"></i>
|
||||
<span data-i18n="tools:markdownToPdf.btnSettings">Settings</span>
|
||||
</button>
|
||||
<button class="md-editor-btn md-editor-btn-primary" id="mdExport">
|
||||
<i data-lucide="download"></i>
|
||||
<span data-i18n="tools:markdownToPdf.btnExportPdf">Export PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-editor-main">
|
||||
<div class="md-editor-pane">
|
||||
<div class="md-editor-pane-header">
|
||||
<span data-i18n="tools:markdownToPdf.paneMarkdown">Markdown</span>
|
||||
</div>
|
||||
<textarea class="md-editor-textarea" id="mdTextarea" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="md-editor-pane">
|
||||
<div class="md-editor-pane-header">
|
||||
<span data-i18n="tools:markdownToPdf.panePreview">Preview</span>
|
||||
</div>
|
||||
<div class="md-editor-preview" id="mdPreview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal (hidden by default) -->
|
||||
<div class="md-editor-modal-overlay" id="mdSettingsModal" style="display: none;">
|
||||
<div class="md-editor-modal">
|
||||
<div class="md-editor-modal-header">
|
||||
<h2 class="md-editor-modal-title" data-i18n="tools:markdownToPdf.settingsTitle">Markdown Settings</h2>
|
||||
<button class="md-editor-modal-close" id="mdCloseSettings">
|
||||
<i data-lucide="x" width="20" height="20"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md-editor-settings-group">
|
||||
<h3 data-i18n="tools:markdownToPdf.settingsPreset">Preset</h3>
|
||||
<select id="mdPreset">
|
||||
<option value="default" selected data-i18n="tools:markdownToPdf.presetDefault">Default (GFM-like)</option>
|
||||
<option value="commonmark" data-i18n="tools:markdownToPdf.presetCommonmark">CommonMark (strict)</option>
|
||||
<option value="zero" data-i18n="tools:markdownToPdf.presetZero">Minimal (no features)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md-editor-settings-group">
|
||||
<h3 data-i18n="tools:markdownToPdf.settingsOptions">Markdown Options</h3>
|
||||
<label class="md-editor-checkbox">
|
||||
<input type="checkbox" id="mdOptHtml" ${this.mdOptions.html ? 'checked' : ''} />
|
||||
<span data-i18n="tools:markdownToPdf.optAllowHtml">Allow HTML tags</span>
|
||||
</label>
|
||||
<label class="md-editor-checkbox">
|
||||
<input type="checkbox" id="mdOptBreaks" ${this.mdOptions.breaks ? 'checked' : ''} />
|
||||
<span data-i18n="tools:markdownToPdf.optBreaks">Convert newlines to <br></span>
|
||||
</label>
|
||||
<label class="md-editor-checkbox">
|
||||
<input type="checkbox" id="mdOptLinkify" ${this.mdOptions.linkify ? 'checked' : ''} />
|
||||
<span data-i18n="tools:markdownToPdf.optLinkify">Auto-convert URLs to links</span>
|
||||
</label>
|
||||
<label class="md-editor-checkbox">
|
||||
<input type="checkbox" id="mdOptTypographer" ${this.mdOptions.typographer ? 'checked' : ''} />
|
||||
<span data-i18n="tools:markdownToPdf.optTypographer">Typographer (smart quotes, etc.)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.editor = document.getElementById('mdTextarea') as HTMLTextAreaElement;
|
||||
this.preview = document.getElementById('mdPreview') as HTMLElement;
|
||||
|
||||
this.setupEventListeners();
|
||||
this.applyI18n();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof (window as any).lucide !== 'undefined') {
|
||||
(window as any).lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Editor input
|
||||
this.editor?.addEventListener('input', () => {
|
||||
this.updatePreview();
|
||||
});
|
||||
|
||||
// Sync scroll
|
||||
const syncScrollBtn = document.getElementById('mdSyncScroll');
|
||||
syncScrollBtn?.addEventListener('click', () => {
|
||||
this.syncScroll = !this.syncScroll;
|
||||
syncScrollBtn.classList.toggle('md-editor-btn-primary');
|
||||
syncScrollBtn.classList.toggle('md-editor-btn-secondary');
|
||||
});
|
||||
|
||||
// Editor scroll sync
|
||||
this.editor?.addEventListener('scroll', () => {
|
||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||
this.isSyncing = true;
|
||||
const scrollPercentage = this.editor.scrollTop / (this.editor.scrollHeight - this.editor.clientHeight);
|
||||
this.preview.scrollTop = scrollPercentage * (this.preview.scrollHeight - this.preview.clientHeight);
|
||||
setTimeout(() => this.isSyncing = false, 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Preview scroll sync (bidirectional)
|
||||
this.preview?.addEventListener('scroll', () => {
|
||||
if (this.syncScroll && !this.isSyncing && this.editor && this.preview) {
|
||||
this.isSyncing = true;
|
||||
const scrollPercentage = this.preview.scrollTop / (this.preview.scrollHeight - this.preview.clientHeight);
|
||||
this.editor.scrollTop = scrollPercentage * (this.editor.scrollHeight - this.editor.clientHeight);
|
||||
setTimeout(() => this.isSyncing = false, 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const editorContainer = document.querySelector('.md-editor');
|
||||
themeToggle?.addEventListener('click', () => {
|
||||
editorContainer?.classList.toggle('light-mode');
|
||||
themeToggle.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Settings modal open
|
||||
document.getElementById('mdSettings')?.addEventListener('click', () => {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
// Settings modal close
|
||||
document.getElementById('mdCloseSettings')?.addEventListener('click', () => {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
document.getElementById('mdSettingsModal')?.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).classList.contains('md-editor-modal-overlay')) {
|
||||
const modal = document.getElementById('mdSettingsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Settings checkboxes
|
||||
document.getElementById('mdOptHtml')?.addEventListener('change', (e) => {
|
||||
this.mdOptions.html = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
document.getElementById('mdOptBreaks')?.addEventListener('change', (e) => {
|
||||
this.mdOptions.breaks = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
document.getElementById('mdOptLinkify')?.addEventListener('change', (e) => {
|
||||
this.mdOptions.linkify = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
document.getElementById('mdOptTypographer')?.addEventListener('change', (e) => {
|
||||
this.mdOptions.typographer = (e.target as HTMLInputElement).checked;
|
||||
this.updateMarkdownIt();
|
||||
});
|
||||
|
||||
// Preset selector
|
||||
document.getElementById('mdPreset')?.addEventListener('change', (e) => {
|
||||
const preset = (e.target as HTMLSelectElement).value;
|
||||
this.applyPreset(preset as 'default' | 'commonmark' | 'zero');
|
||||
});
|
||||
|
||||
// Upload button
|
||||
document.getElementById('mdUpload')?.addEventListener('click', () => {
|
||||
document.getElementById('mdFileInput')?.click();
|
||||
});
|
||||
|
||||
// File input change
|
||||
document.getElementById('mdFileInput')?.addEventListener('change', (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
this.loadFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Export PDF
|
||||
document.getElementById('mdExport')?.addEventListener('click', () => {
|
||||
this.exportPdf();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
this.editor?.addEventListener('keydown', (e) => {
|
||||
// Ctrl/Cmd + S to export
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
this.exportPdf();
|
||||
}
|
||||
// Tab key for indentation
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.editor!.selectionStart;
|
||||
const end = this.editor!.selectionEnd;
|
||||
const value = this.editor!.value;
|
||||
this.editor!.value = value.substring(0, start) + ' ' + value.substring(end);
|
||||
this.editor!.selectionStart = this.editor!.selectionEnd = start + 2;
|
||||
this.updatePreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private currentPreset: 'default' | 'commonmark' | 'zero' = 'default';
|
||||
|
||||
private applyPreset(preset: 'default' | 'commonmark' | 'zero'): void {
|
||||
this.currentPreset = preset;
|
||||
|
||||
// Update options based on preset
|
||||
if (preset === 'commonmark') {
|
||||
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
|
||||
} else if (preset === 'zero') {
|
||||
this.mdOptions = { html: false, breaks: false, linkify: false, typographer: false };
|
||||
} else {
|
||||
this.mdOptions = { html: true, breaks: false, linkify: true, typographer: true };
|
||||
}
|
||||
|
||||
// Update UI checkboxes
|
||||
(document.getElementById('mdOptHtml') as HTMLInputElement).checked = this.mdOptions.html;
|
||||
(document.getElementById('mdOptBreaks') as HTMLInputElement).checked = this.mdOptions.breaks;
|
||||
(document.getElementById('mdOptLinkify') as HTMLInputElement).checked = this.mdOptions.linkify;
|
||||
(document.getElementById('mdOptTypographer') as HTMLInputElement).checked = this.mdOptions.typographer;
|
||||
|
||||
this.updateMarkdownIt();
|
||||
}
|
||||
|
||||
private async loadFile(file: File): Promise<void> {
|
||||
try {
|
||||
const text = await file.text();
|
||||
this.setContent(text);
|
||||
} catch (error) {
|
||||
console.error('Failed to load file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private createMarkdownIt(): MarkdownIt {
|
||||
// Use preset if commonmark or zero
|
||||
let md: MarkdownIt;
|
||||
if (this.currentPreset === 'commonmark') {
|
||||
md = new MarkdownIt('commonmark');
|
||||
} else if (this.currentPreset === 'zero') {
|
||||
md = new MarkdownIt('zero');
|
||||
// Enable basic features for zero preset
|
||||
md.enable(['paragraph', 'newline', 'text']);
|
||||
} else {
|
||||
md = new MarkdownIt({
|
||||
...this.mdOptions,
|
||||
highlight: (str: string, lang: string) => {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
|
||||
} catch {
|
||||
// Fall through to default
|
||||
}
|
||||
}
|
||||
return ''; // Use external default escaping
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply plugins only for default preset (plugins may not work well with commonmark/zero)
|
||||
if (this.currentPreset === 'default') {
|
||||
md.use(sub) // Subscript: ~text~ -> <sub>text</sub>
|
||||
.use(sup) // Superscript: ^text^ -> <sup>text</sup>
|
||||
.use(footnote) // Footnotes: [^1] and [^1]: footnote text
|
||||
.use(deflist) // Definition lists
|
||||
.use(abbr) // Abbreviations: *[abbr]: full text
|
||||
.use(emoji) // Emoji: :smile: -> 😄
|
||||
.use(ins) // Inserted text: ++text++ -> <ins>text</ins>
|
||||
.use(mark) // Marked text: ==text== -> <mark>text</mark>
|
||||
.use(taskLists, { enabled: true, label: true, labelAfter: true }) // Task lists: - [x] done
|
||||
.use(anchor, { permalink: false }) // Header anchors
|
||||
.use(tocDoneRight); // Table of contents: ${toc}
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
private updateMarkdownIt(): void {
|
||||
this.md = this.createMarkdownIt();
|
||||
this.configureLinkRenderer();
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
private updatePreview(): void {
|
||||
if (!this.editor || !this.preview) return;
|
||||
|
||||
const markdown = this.editor.value;
|
||||
const html = this.md.render(markdown);
|
||||
this.preview.innerHTML = html;
|
||||
}
|
||||
|
||||
public setContent(content: string): void {
|
||||
if (this.editor) {
|
||||
this.editor.value = content;
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
public getContent(): string {
|
||||
return this.editor?.value || '';
|
||||
}
|
||||
|
||||
public getHtml(): string {
|
||||
return this.md.render(this.getContent());
|
||||
}
|
||||
|
||||
private exportPdf(): void {
|
||||
// Use browser's native print functionality
|
||||
window.print();
|
||||
}
|
||||
|
||||
private getStyledHtml(): string {
|
||||
const content = this.getHtml();
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
h1 { font-size: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
||||
h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
||||
h3 { font-size: 1.25em; }
|
||||
h4 { font-size: 1em; }
|
||||
p { margin: 1em 0; }
|
||||
a { color: #0366d6; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
background: #f6f8fa;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
pre {
|
||||
background: #f6f8fa;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
border-radius: 6px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 4px solid #dfe2e5;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
li { margin: 0.25em 0; }
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) { background: #f6f8fa; }
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
margin: 2em 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
/* Syntax highlighting - GitHub style */
|
||||
.hljs {
|
||||
color: #24292e;
|
||||
background: #f6f8fa;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: #d73a49;
|
||||
}
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag .hljs-attr {
|
||||
color: #005cc5;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-doctag {
|
||||
color: #032f62;
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-selector-id {
|
||||
color: #6f42c1;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title {
|
||||
color: #6f42c1;
|
||||
}
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: #22863a;
|
||||
}
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: #032f62;
|
||||
}
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #e36209;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: #005cc5;
|
||||
}
|
||||
.hljs-meta {
|
||||
color: #6a737d;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-deletion {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
.hljs-addition {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
/* Plugin styles */
|
||||
mark {
|
||||
background-color: #fff3cd;
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ins {
|
||||
text-decoration: none;
|
||||
background-color: #d4edda;
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
sub, sup {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.task-list-item {
|
||||
list-style-type: none;
|
||||
margin-left: -1.5em;
|
||||
}
|
||||
.task-list-item input[type="checkbox"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.footnotes {
|
||||
margin-top: 2em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.footnotes-sep {
|
||||
display: none;
|
||||
}
|
||||
.footnote-ref {
|
||||
font-size: 0.75em;
|
||||
vertical-align: super;
|
||||
}
|
||||
.footnote-backref {
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
dl {
|
||||
margin: 1em 0;
|
||||
}
|
||||
dt {
|
||||
font-weight: 600;
|
||||
margin-top: 1em;
|
||||
}
|
||||
dd {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.25em;
|
||||
color: #6a737d;
|
||||
}
|
||||
abbr {
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
}
|
||||
.table-of-contents {
|
||||
background: #f6f8fa;
|
||||
padding: 1em 1.5em;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.table-of-contents ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
.table-of-contents li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private applyI18n(): void {
|
||||
// Apply translations to elements within this component
|
||||
applyTranslations();
|
||||
|
||||
// Special handling for select options (data-i18n on options doesn't work with applyTranslations)
|
||||
const presetSelect = document.getElementById('mdPreset') as HTMLSelectElement;
|
||||
if (presetSelect) {
|
||||
const options = presetSelect.querySelectorAll('option[data-i18n]');
|
||||
options.forEach((option) => {
|
||||
const key = option.getAttribute('data-i18n');
|
||||
if (key) {
|
||||
// Use i18next directly for option text
|
||||
const translated = (window as any).i18next?.t(key);
|
||||
if (translated && translated !== key) {
|
||||
option.textContent = translated;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
129
src/js/utils/wasm-preloader.ts
Normal file
129
src/js/utils/wasm-preloader.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { getLibreOfficeConverter } from './libreoffice-loader.js';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
import loadGsWASM from '@bentopdf/gs-wasm';
|
||||
import { setCachedGsModule } from './ghostscript-loader.js';
|
||||
|
||||
export enum PreloadStatus {
|
||||
IDLE = 'idle',
|
||||
LOADING = 'loading',
|
||||
READY = 'ready',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
interface PreloadState {
|
||||
libreoffice: PreloadStatus;
|
||||
pymupdf: PreloadStatus;
|
||||
ghostscript: PreloadStatus;
|
||||
}
|
||||
|
||||
const preloadState: PreloadState = {
|
||||
libreoffice: PreloadStatus.IDLE,
|
||||
pymupdf: PreloadStatus.IDLE,
|
||||
ghostscript: PreloadStatus.IDLE
|
||||
};
|
||||
|
||||
let pymupdfInstance: PyMuPDF | null = null;
|
||||
|
||||
export function getPreloadStatus(): Readonly<PreloadState> {
|
||||
return { ...preloadState };
|
||||
}
|
||||
|
||||
export function getPymupdfInstance(): PyMuPDF | null {
|
||||
return pymupdfInstance;
|
||||
}
|
||||
|
||||
async function preloadLibreOffice(): Promise<void> {
|
||||
if (preloadState.libreoffice !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.libreoffice = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting LibreOffice WASM preload...');
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
await converter.initialize();
|
||||
preloadState.libreoffice = PreloadStatus.READY;
|
||||
console.log('[Preloader] LibreOffice WASM ready');
|
||||
} catch (e) {
|
||||
preloadState.libreoffice = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] LibreOffice preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadPyMuPDF(): Promise<void> {
|
||||
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.pymupdf = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting PyMuPDF preload...');
|
||||
|
||||
try {
|
||||
pymupdfInstance = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
|
||||
await pymupdfInstance.load();
|
||||
preloadState.pymupdf = PreloadStatus.READY;
|
||||
console.log('[Preloader] PyMuPDF ready');
|
||||
} catch (e) {
|
||||
preloadState.pymupdf = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] PyMuPDF preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadGhostscript(): Promise<void> {
|
||||
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.ghostscript = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
||||
|
||||
try {
|
||||
const gsModule = await loadGsWASM({
|
||||
locateFile: (path: string) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
return import.meta.env.BASE_URL + 'ghostscript-wasm/gs.wasm';
|
||||
}
|
||||
return path;
|
||||
},
|
||||
print: () => { },
|
||||
printErr: () => { },
|
||||
});
|
||||
setCachedGsModule(gsModule as any);
|
||||
preloadState.ghostscript = PreloadStatus.READY;
|
||||
console.log('[Preloader] Ghostscript WASM ready');
|
||||
} catch (e) {
|
||||
preloadState.ghostscript = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] Ghostscript preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleIdleTask(task: () => Promise<void>): void {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => task(), { timeout: 5000 });
|
||||
} else {
|
||||
setTimeout(() => task(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export function startBackgroundPreload(): void {
|
||||
console.log('[Preloader] Scheduling background WASM preloads...');
|
||||
|
||||
const libreOfficePages = [
|
||||
'word-to-pdf', 'excel-to-pdf', 'ppt-to-pdf', 'powerpoint-to-pdf',
|
||||
'docx-to-pdf', 'xlsx-to-pdf', 'pptx-to-pdf', 'csv-to-pdf',
|
||||
'rtf-to-pdf', 'odt-to-pdf', 'ods-to-pdf', 'odp-to-pdf'
|
||||
];
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const isLibreOfficePage = libreOfficePages.some(page => currentPath.includes(page));
|
||||
|
||||
if (isLibreOfficePage) {
|
||||
console.log('[Preloader] Skipping preloads on LibreOffice page to save memory');
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleIdleTask(async () => {
|
||||
console.log('[Preloader] Starting sequential WASM preloads...');
|
||||
|
||||
await preloadPyMuPDF();
|
||||
await preloadGhostscript();
|
||||
|
||||
console.log('[Preloader] Sequential preloads complete (LibreOffice skipped - loaded on demand)');
|
||||
});
|
||||
}
|
||||
|
||||
196
src/js/utils/xml-to-pdf.ts
Normal file
196
src/js/utils/xml-to-pdf.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
export interface XmlToPdfOptions {
|
||||
onProgress?: (percent: number, message: string) => void;
|
||||
}
|
||||
|
||||
interface jsPDFWithAutoTable extends jsPDF {
|
||||
lastAutoTable?: { finalY: number };
|
||||
}
|
||||
|
||||
export async function convertXmlToPdf(
|
||||
file: File,
|
||||
options?: XmlToPdfOptions
|
||||
): Promise<Blob> {
|
||||
const { onProgress } = options || {};
|
||||
|
||||
onProgress?.(10, 'Reading XML file...');
|
||||
const xmlText = await file.text();
|
||||
|
||||
onProgress?.(30, 'Parsing XML structure...');
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||||
|
||||
const parseError = xmlDoc.querySelector('parsererror');
|
||||
if (parseError) {
|
||||
throw new Error('Invalid XML: ' + parseError.textContent);
|
||||
}
|
||||
|
||||
onProgress?.(50, 'Analyzing data structure...');
|
||||
|
||||
const doc: jsPDFWithAutoTable = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
let yPosition = 20;
|
||||
|
||||
const root = xmlDoc.documentElement;
|
||||
const rootName = formatTitle(root.tagName);
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' });
|
||||
yPosition += 15;
|
||||
|
||||
onProgress?.(60, 'Generating formatted content...');
|
||||
|
||||
const children = Array.from(root.children);
|
||||
|
||||
if (children.length > 0) {
|
||||
const groups = groupByTagName(children);
|
||||
|
||||
for (const [groupName, elements] of Object.entries(groups)) {
|
||||
const { headers, rows } = extractTableData(elements);
|
||||
|
||||
if (headers.length > 0 && rows.length > 0) {
|
||||
if (Object.keys(groups).length > 1) {
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(formatTitle(groupName), 14, yPosition);
|
||||
yPosition += 8;
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
head: [headers.map(h => formatTitle(h))],
|
||||
body: rows,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 4,
|
||||
overflow: 'linebreak',
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [243, 244, 246],
|
||||
},
|
||||
margin: { top: 20, left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
didDrawPage: (data) => {
|
||||
yPosition = (data.cursor?.y || yPosition) + 10;
|
||||
}
|
||||
});
|
||||
|
||||
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const kvPairs = extractKeyValuePairs(root);
|
||||
if (kvPairs.length > 0) {
|
||||
autoTable(doc, {
|
||||
head: [['Property', 'Value']],
|
||||
body: kvPairs,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 10,
|
||||
cellPadding: 5,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 60 },
|
||||
1: { cellWidth: 'auto' },
|
||||
},
|
||||
margin: { left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(90, 'Finalizing PDF...');
|
||||
|
||||
const pdfBlob = doc.output('blob');
|
||||
|
||||
onProgress?.(100, 'Complete!');
|
||||
return pdfBlob;
|
||||
}
|
||||
|
||||
|
||||
function groupByTagName(elements: Element[]): Record<string, Element[]> {
|
||||
const groups: Record<string, Element[]> = {};
|
||||
|
||||
for (const element of elements) {
|
||||
const tagName = element.tagName;
|
||||
if (!groups[tagName]) {
|
||||
groups[tagName] = [];
|
||||
}
|
||||
groups[tagName].push(element);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function extractTableData(elements: Element[]): { headers: string[], rows: string[][] } {
|
||||
if (elements.length === 0) {
|
||||
return { headers: [], rows: [] };
|
||||
}
|
||||
|
||||
const headerSet = new Set<string>();
|
||||
for (const element of elements) {
|
||||
for (const child of Array.from(element.children)) {
|
||||
headerSet.add(child.tagName);
|
||||
}
|
||||
}
|
||||
const headers = Array.from(headerSet);
|
||||
|
||||
const rows: string[][] = [];
|
||||
for (const element of elements) {
|
||||
const row: string[] = [];
|
||||
for (const header of headers) {
|
||||
const child = element.querySelector(header);
|
||||
row.push(child?.textContent?.trim() || '');
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
|
||||
function extractKeyValuePairs(element: Element): string[][] {
|
||||
const pairs: string[][] = [];
|
||||
|
||||
for (const child of Array.from(element.children)) {
|
||||
const key = child.tagName;
|
||||
const value = child.textContent?.trim() || '';
|
||||
if (value) {
|
||||
pairs.push([formatTitle(key), value]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
pairs.push([formatTitle(attr.name), attr.value]);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
function formatTitle(tagName: string): string {
|
||||
return tagName
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
@@ -97,8 +97,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
229
src/pages/cbz-to-pdf.html
Normal file
229
src/pages/cbz-to-pdf.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CBZ to PDF - Convert XPS/OCBZ to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert XPS and OXPS documents to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/cbz-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/cbz-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/cbz-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/cbz-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:cbzToPdf.name">CBZ to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:cbzToPdf.subtitle">
|
||||
Convert comic book archives (CBZ/CBR) to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:cbzToPdf.acceptedFormats">CBZ, CBR files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".cbz,.cbr,application/x-cbz,application/x-cbr,application/vnd.comicbook+zip,application/vnd.comicbook-rar"
|
||||
multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6"
|
||||
data-i18n="tools:cbzToPdf.convertButton">Convert to PDF</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/cbz-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,37 +99,105 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="compress-options" class="hidden mt-6 space-y-6">
|
||||
<div>
|
||||
<label for="compression-level" class="block mb-2 text-sm font-medium text-gray-300">Compression
|
||||
Level</label>
|
||||
<select id="compression-level"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="balanced">Balanced (Recommended)</option>
|
||||
<option value="high-quality">High Quality (Larger file)</option>
|
||||
<option value="small-size">Smallest Size (Lower quality)</option>
|
||||
<option value="extreme">Extreme (Very low quality)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="compression-algorithm" class="block mb-2 text-sm font-medium text-gray-300">Compression
|
||||
Algorithm</label>
|
||||
<select id="compression-algorithm"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="vector">Vector (For Text Heavy PDF)</option>
|
||||
<option value="photon">Photon (For Complex Images & Drawings)</option>
|
||||
<option value="condense"> Condense (Recommended)</option>
|
||||
<option value="photon"> Photon (For Photo-Heavy PDFs)</option>
|
||||
</select>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
Choose 'Vector' for text based PDFs, or 'Photon' for scanned documents and complex images.
|
||||
</p>
|
||||
<div id="algorithm-info" class="mt-2 text-xs text-gray-400">
|
||||
<p id="condense-info"><strong>Condense</strong> uses advanced compression: removes dead-weight,
|
||||
optimizes images, subsets fonts. Best for most PDFs.</p>
|
||||
<p id="photon-info" class="hidden"><strong>Photon</strong> converts pages to images. Use for
|
||||
photo-heavy/scanned PDFs. <span class="text-yellow-500">⚠️ Warning: Text will become
|
||||
non-selectable and links will stop working.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="compression-level" class="block mb-2 text-sm font-medium text-gray-300">Compression
|
||||
Level</label>
|
||||
<select id="compression-level"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="light">Light (Preserve Quality)</option>
|
||||
<option value="balanced" selected>Balanced (Recommended)</option>
|
||||
<option value="aggressive">Aggressive (Smaller Files)</option>
|
||||
<option value="extreme">Extreme (Maximum Compression)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Grayscale toggle - always visible -->
|
||||
<div class="flex items-center gap-3 p-3 bg-gray-800 rounded-lg border border-gray-700">
|
||||
<input type="checkbox" id="convert-to-grayscale"
|
||||
class="w-5 h-5 rounded cursor-pointer flex-shrink-0">
|
||||
<div>
|
||||
<label for="convert-to-grayscale"
|
||||
class="text-sm font-medium text-gray-300 cursor-pointer">Convert to Grayscale</label>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Reduces file size by removing color information</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="toggle-custom-settings" type="button"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 text-sm font-medium">
|
||||
<i data-lucide="settings" class="w-4 h-4"></i>
|
||||
<span>Custom Settings</span>
|
||||
<i data-lucide="chevron-down" id="custom-settings-chevron" class="w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
<div id="custom-settings-panel"
|
||||
class="hidden mt-4 p-4 bg-gray-900 rounded-lg border border-gray-700 space-y-4">
|
||||
<div class="text-sm text-gray-400 mb-2">Fine-tune compression parameters:</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="image-quality" class="block mb-1 text-xs font-medium text-gray-400">Output
|
||||
Quality</label>
|
||||
<input type="number" id="image-quality" value="75" min="1" max="100"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">1-100%</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="dpi-target" class="block mb-1 text-xs font-medium text-gray-400">Resize
|
||||
Images To</label>
|
||||
<input type="number" id="dpi-target" value="96" min="36" max="300"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">DPI</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="dpi-threshold" class="block mb-1 text-xs font-medium text-gray-400">Only
|
||||
Process Above</label>
|
||||
<input type="number" id="dpi-threshold" value="150" min="72" max="600"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">DPI</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="remove-metadata" checked class="w-4 h-4 rounded">
|
||||
<span class="text-sm text-gray-300">Remove metadata</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="subset-fonts" checked class="w-4 h-4 rounded">
|
||||
<span class="text-sm text-gray-300">Subset fonts (remove unused glyphs)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="remove-thumbnails" checked class="w-4 h-4 rounded">
|
||||
<span class="text-sm text-gray-300">Remove embedded thumbnails</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Compress PDF</button>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
229
src/pages/csv-to-pdf.html
Normal file
229
src/pages/csv-to-pdf.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CSV to PDF - Convert Spreadsheets to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert CSV (Comma-Separated Values) spreadsheet files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/csv-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/csv-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/csv-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/csv-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">CSV to PDF</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert CSV (Comma-Separated Values) spreadsheet files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">One or more CSV files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".csv,text/csv" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Convert to PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/csv-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -10,8 +10,8 @@
|
||||
<link rel="alternate" hreflang="en" href="/en/delete-pages.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/delete-pages.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/delete-pages.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/delete-pages.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/delete-pages.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,12 +41,15 @@
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:deletePages.name">Delete Pages</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:deletePages.subtitle">Remove specific pages or ranges of pages from your PDF file.</p>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:deletePages.subtitle">Remove specific pages or ranges of
|
||||
pages from your PDF file.</p>
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold" data-i18n="upload.clickToSelect">Click to select a file</span> <span data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF Documents</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
@@ -68,15 +71,18 @@
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing" data-i18n="loader.processing">Processing...</p>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing"
|
||||
data-i18n="loader.processing">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title" data-i18n="alert.title">Alert</h3>
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg" data-i18n="alert.ok" data-i18n="alert.ok">OK</button>
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg"
|
||||
data-i18n="alert.ok" data-i18n="alert.ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
@@ -84,7 +90,7 @@
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/delete-pages-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -100,10 +100,20 @@
|
||||
<ul class="list-disc list-inside text-xs text-gray-400 mt-1 space-y-1">
|
||||
<li><strong>Vertical:</strong> Split each page into left and right halves.</li>
|
||||
<li><strong>Horizontal:</strong> Split each page into top and bottom halves.</li>
|
||||
<li>The result will have twice as many pages as the original.</li>
|
||||
<li>Specify which pages to divide. Other pages will be kept as-is.</li>
|
||||
<li>Leave blank or use "all" to divide all pages.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="page-range" class="block mb-2 text-sm font-medium text-gray-300">Pages to Divide</label>
|
||||
<input type="text" id="page-range"
|
||||
class="bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 block w-full p-2.5"
|
||||
placeholder="e.g. 1-5, 8, 11-13 (leave blank for all pages)">
|
||||
<p class="mt-1 text-xs text-gray-400">Enter page numbers separated by commas or ranges with hyphens.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="split-type" class="block mb-2 text-sm font-medium text-gray-300">Split Direction</label>
|
||||
<select id="split-type"
|
||||
|
||||
@@ -90,13 +90,13 @@
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="embed-pdf-wrapper" class="hidden mt-6 w-full h-[75vh] border border-gray-600 rounded-lg">
|
||||
<div id="embed-pdf-container" class="w-full h-full"></div>
|
||||
<div id="embed-pdf-wrapper" class="hidden mt-6">
|
||||
<div id="embed-pdf-container" class="w-full h-[75vh] border border-gray-600 rounded-lg"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
230
src/pages/epub-to-pdf.html
Normal file
230
src/pages/epub-to-pdf.html
Normal file
@@ -0,0 +1,230 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EPUB to PDF - Convert EPUB to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert EPUB e-books to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/epub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/epub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/epub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/epub-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:epubToPdf.name">EPUB to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:epubToPdf.subtitle">
|
||||
Convert EPUB e-books to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:epubToPdf.acceptedFormats">EPUB files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".epub,application/epub+zip" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full" data-i18n="tools:epubToPdf.convertButton">Convert
|
||||
to PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/epub-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
232
src/pages/excel-to-pdf.html
Normal file
232
src/pages/excel-to-pdf.html
Normal file
@@ -0,0 +1,232 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Excel to PDF - Convert XLSX, XLS to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/excel-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/excel-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/excel-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/excel-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:excelToPdf.name">Excel to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:excelToPdf.subtitle">
|
||||
Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:excelToPdf.acceptedFormats">XLSX, XLS, ODS, CSV
|
||||
files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".xls,.xlsx,.ods,.csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet,text/csv"
|
||||
multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full" data-i18n="tools:excelToPdf.convertButton">Convert
|
||||
to PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/excel-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
190
src/pages/extract-images.html
Normal file
190
src/pages/extract-images.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Extract Images from PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract all images from PDF files. Download individual images or all as a ZIP. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/extract-images.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/extract-images.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-4xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Extract Images from PDF</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Extract all embedded images from PDF files. Download individually or as a ZIP archive.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span>Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="extract-options" class="hidden mt-6 space-y-6">
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Extract Images</button>
|
||||
</div>
|
||||
|
||||
<div id="images-container" class="hidden mt-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-white">Extracted Images</h2>
|
||||
<button id="download-all-btn"
|
||||
class="btn bg-green-600 hover:bg-green-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="download"></i> <span>Download All (ZIP)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="images-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/nickvidal/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<i data-lucide="github"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/extract-images-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
208
src/pages/extract-tables.html
Normal file
208
src/pages/extract-tables.html
Normal file
@@ -0,0 +1,208 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Extract PDF Tables - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract tables from PDF files and export as CSV, JSON, or Markdown. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/extract-tables.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/extract-tables.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/extract-tables.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/extract-tables.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/extract-tables.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:extractTables.name">Extract PDF Tables</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:extractTables.subtitle">
|
||||
Extract tables from PDF files and export as CSV, JSON, or Markdown.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF file</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="options-panel" class="hidden mt-6 space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-300">Export Format</label>
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="export-format" value="csv" checked
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-300">CSV</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="export-format" value="json"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-300">JSON</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="export-format" value="markdown"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-300">Markdown</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full">Extract Tables</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/extract-tables-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
228
src/pages/fb2-to-pdf.html
Normal file
228
src/pages/fb2-to-pdf.html
Normal file
@@ -0,0 +1,228 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FB2 to PDF - Convert XPS/OFB2 to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert XPS and OXPS documents to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/fb2-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/fb2-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/fb2-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/fb2-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:fb2ToPdf.name">FB2 to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:fb2ToPdf.subtitle">
|
||||
Convert FictionBook (FB2) e-books to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:fb2ToPdf.acceptedFormats">FB2 files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".fb2,application/x-fictionbook+xml,text/xml" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6"
|
||||
data-i18n="tools:fb2ToPdf.convertButton">Convert to PDF</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/fb2-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -98,8 +98,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -198,8 +198,9 @@
|
||||
<!-- Canvas Area -->
|
||||
<main class="flex-1 order-1 lg:order-2 min-w-0">
|
||||
<!-- Page Management Toolbar -->
|
||||
<div class="bg-gray-700 rounded-lg p-3 mb-4 flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="bg-gray-700 rounded-lg p-3 mb-4 flex flex-wrap gap-2 justify-center md:justify-between items-center overflow-hidden">
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 min-w-0">
|
||||
<button id="prevPageBtn"
|
||||
class="bg-gray-600 hover:bg-gray-500 text-white p-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled>
|
||||
@@ -211,14 +212,14 @@
|
||||
disabled>
|
||||
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 ml-2 border-l border-gray-600 pl-2">
|
||||
<div class="flex items-center gap-2 md:ml-2 md:border-l border-gray-600 md:pl-2">
|
||||
<span class="text-sm text-gray-400">Go to:</span>
|
||||
<input type="number" id="gotoPageInput" min="1"
|
||||
class="w-16 bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<button id="gotoPageBtn"
|
||||
class="bg-indigo-600 hover:bg-indigo-500 text-white px-2 py-1 rounded text-sm transition-colors">Go</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-2 border-l border-gray-600 pl-2">
|
||||
<div class="hidden md:flex items-center gap-2 md:ml-2 md:border-l border-gray-600 md:pl-2">
|
||||
<input type="checkbox" id="enableGridCheckbox" checked class="mr-1">
|
||||
<label for="enableGridCheckbox" class="text-sm text-gray-400">Grid:</label>
|
||||
<input type="number" id="gridVInput" min="2" max="14" value="2" placeholder="V"
|
||||
@@ -233,7 +234,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button id="resetBtn"
|
||||
class="bg-gray-600 hover:bg-gray-500 text-white p-2 px-3 rounded flex items-center gap-2 transition-colors">
|
||||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i>
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Image to PDF - BentoPDF</title>
|
||||
<title>Images to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert multiple images (JPG, PNG, WebP, SVG) into a single PDF file. Free, secure, and runs entirely in your browser.">
|
||||
content="Convert images (JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JP2, PSD, SVG, HEIC, WebP) to PDF. Free, secure, runs in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/image-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/image-to-pdf.html" />
|
||||
@@ -69,7 +69,7 @@
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:imageToPdf.name">Image to PDF</h1>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:imageToPdf.name">Images to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:imageToPdf.subtitle">Convert one or more images into a single
|
||||
PDF file.</p>
|
||||
|
||||
@@ -80,12 +80,14 @@
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">All Image Types</p>
|
||||
<p id="supported-formats" class="text-xs text-gray-500">JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM,
|
||||
PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
multiple accept="image/*">
|
||||
multiple
|
||||
accept=".jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.pnm,.pgm,.pbm,.ppm,.pam,.jxr,.jpx,.jp2,.psd,.svg,.heic,.heif,.webp">
|
||||
</div>
|
||||
|
||||
<div id="file-controls" class="hidden mt-4 flex gap-3">
|
||||
@@ -94,8 +96,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:jpgToPdf.name">JPG to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:jpgToPdf.subtitle">
|
||||
Convert one or more JPG images into a single PDF file.
|
||||
Create a PDF from JPG, JPEG, and JPEG2000 (JP2/JPX) images.
|
||||
</p>
|
||||
|
||||
<!-- Drop Zone -->
|
||||
@@ -86,12 +86,12 @@
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">JPG Images</p>
|
||||
<p class="text-xs text-gray-500">JPG, JPEG, JP2, JPX Images</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
multiple accept="image/jpeg, image/jpg">
|
||||
multiple accept="image/jpeg, image/jpg, image/jp2, .jp2, .jpx, .jpeg, .jpg">
|
||||
</div>
|
||||
|
||||
<!-- Control buttons -->
|
||||
@@ -101,8 +101,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
171
src/pages/markdown-to-pdf.html
Normal file
171
src/pages/markdown-to-pdf.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Markdown to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert Markdown to PDF with live preview. Free, secure, and runs entirely in your browser." />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/markdown-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/markdown-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/markdown-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/markdown-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link href="/src/css/markdown-editor.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="min-h-screen py-8 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="bg-gray-800 rounded-xl shadow-xl p-6 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Markdown to PDF</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Write markdown with live preview. Supports syntax highlighting, tables, emoji, and more. Print to
|
||||
save as PDF.
|
||||
</p>
|
||||
|
||||
<!-- Editor container -->
|
||||
<div id="markdown-editor-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/markdown-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -13,7 +13,7 @@
|
||||
<link rel="alternate" hreflang="vi" href="/vi/merge-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/merge-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
@@ -102,8 +102,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
228
src/pages/mobi-to-pdf.html
Normal file
228
src/pages/mobi-to-pdf.html
Normal file
@@ -0,0 +1,228 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MOBI to PDF - Convert XPS/OMOBI to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert XPS and OXPS documents to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/mobi-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/mobi-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/mobi-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/mobi-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:mobiToPdf.name">MOBI to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:mobiToPdf.subtitle">
|
||||
Convert MOBI e-books to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:mobiToPdf.acceptedFormats">MOBI files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".mobi,application/x-mobipocket-ebook" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6"
|
||||
data-i18n="tools:mobiToPdf.convertButton">Convert to PDF</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/mobi-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
227
src/pages/odg-to-pdf.html
Normal file
227
src/pages/odg-to-pdf.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ODG to PDF - Convert OpenDocument Graphics to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert OpenDocument Graphics (ODG) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/odg-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/odg-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/odg-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/odg-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/odg-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:odgToPdf.name">ODG to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:odgToPdf.subtitle">
|
||||
Convert OpenDocument Graphics (ODG) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:odgToPdf.acceptedFormats">ODG files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".odg" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:odgToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs mt-6">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0"></i>
|
||||
<p data-i18n="tools.firstLoadNotice">
|
||||
First load takes a moment as we download our conversion engine. After that, all loads will be
|
||||
instant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/odg-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
227
src/pages/odp-to-pdf.html
Normal file
227
src/pages/odp-to-pdf.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ODP to PDF - Convert OpenDocument Presentation to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert OpenDocument Presentation (ODP) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/odp-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/odp-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/odp-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/odp-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/odp-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:odpToPdf.name">ODP to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:odpToPdf.subtitle">
|
||||
Convert OpenDocument Presentation (ODP) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:odpToPdf.acceptedFormats">ODP files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".odp" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:odpToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs mt-6">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0"></i>
|
||||
<p data-i18n="tools.firstLoadNotice">
|
||||
First load takes a moment as we download our conversion engine. After that, all loads will be
|
||||
instant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/odp-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
227
src/pages/ods-to-pdf.html
Normal file
227
src/pages/ods-to-pdf.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ODS to PDF - Convert OpenDocument Spreadsheet to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert OpenDocument Spreadsheet (ODS) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/ods-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/ods-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/ods-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/ods-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/ods-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:odsToPdf.name">ODS to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:odsToPdf.subtitle">
|
||||
Convert OpenDocument Spreadsheet (ODS) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:odsToPdf.acceptedFormats">ODS files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".ods" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:odsToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs mt-6">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0"></i>
|
||||
<p data-i18n="tools.firstLoadNotice">
|
||||
First load takes a moment as we download our conversion engine. After that, all loads will be
|
||||
instant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/ods-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
229
src/pages/odt-to-pdf.html
Normal file
229
src/pages/odt-to-pdf.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ODT to PDF - Convert OpenDocument Text to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert ODT (OpenDocument Text) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/odt-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/odt-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/odt-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/odt-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">ODT to PDF</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert OpenDocument Text (.odt) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">One or more ODT files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".odt,application/vnd.oasis.opendocument.text" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Convert to PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/odt-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/organize-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/organize-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/organize-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/organize-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/organize-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +40,8 @@
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:duplicateOrganize.name">Duplicate and Organize PDF</h1>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:duplicateOrganize.name">Duplicate and
|
||||
Organize PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:duplicateOrganize.subtitle">
|
||||
Drag pages to reorder them. Use the <i data-lucide="copy-plus"
|
||||
class="inline-block w-4 h-4 mx-1 align-text-bottom text-green-500"></i> icon to duplicate a page or
|
||||
@@ -54,9 +55,12 @@
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold" data-i18n="upload.clickToSelect">Click to select a file</span> <span data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF Documents</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your device.</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
@@ -74,15 +78,18 @@
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing" data-i18n="loader.processing">Processing...</p>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing"
|
||||
data-i18n="loader.processing">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title" data-i18n="alert.title">Alert</h3>
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg" data-i18n="alert.ok" data-i18n="alert.ok">OK</button>
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg"
|
||||
data-i18n="alert.ok" data-i18n="alert.ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
@@ -90,7 +97,7 @@
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/organize-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
226
src/pages/pages-to-pdf.html
Normal file
226
src/pages/pages-to-pdf.html
Normal file
@@ -0,0 +1,226 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pages to PDF - Convert WordPerfect to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert Apple Pages documents to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pages-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pages-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pages-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pages-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pagesToPdf.name">Pages to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pagesToPdf.subtitle">
|
||||
Convert Apple Pages documents to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:pagesToPdf.acceptedFormats">Pages files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".pages" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:pagesToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs mt-6">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0"></i>
|
||||
<p data-i18n="tools.firstLoadNotice">
|
||||
First load takes a moment as we download our conversion engine. After that, all loads will be
|
||||
instant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pages-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
340
src/pages/pdf-booklet.html
Normal file
340
src/pages/pdf-booklet.html
Normal file
@@ -0,0 +1,340 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF Booklet - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Create printable booklets from PDF files. Rearrange pages for double-sided printing with live preview. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-booklet.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pdf-booklet.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/pdf-booklet.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pdf-booklet.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-booklet.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-4xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pdfBooklet.name">PDF Booklet</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pdfBooklet.subtitle">
|
||||
Rearrange pages for double-sided booklet printing. Fold and staple to create a booklet.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">A single PDF file</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="tool-options" class="hidden mt-6">
|
||||
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-4">
|
||||
<p class="text-sm text-gray-300"><strong class="text-white"
|
||||
data-i18n="tools:pdfBooklet.howItWorks">How it works:</strong></p>
|
||||
<ul class="list-disc list-inside text-xs text-gray-400 mt-1 space-y-1">
|
||||
<li data-i18n="tools:pdfBooklet.step1">Upload a PDF file.</li>
|
||||
<li data-i18n="tools:pdfBooklet.step2">Pages will be rearranged in booklet order.</li>
|
||||
<li data-i18n="tools:pdfBooklet.step3">Print double-sided, flip on short edge, fold and staple.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4">
|
||||
<label class="block mb-3 text-sm font-medium text-white">Source Rotation</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="rotation" value="none" checked
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>No rotation</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="rotation" value="90cw"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Rotate clockwise (90°)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="rotation" value="90ccw"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Rotate counter-clockwise (90°)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="rotation" value="alternate"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Alternate (odd→CW, even→CCW)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4">
|
||||
<div class="mb-4">
|
||||
<label for="paper-size" class="block mb-2 text-sm font-medium text-white"
|
||||
data-i18n="tools:pdfBooklet.paperSize">Paper Size</label>
|
||||
<select id="paper-size"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2">
|
||||
<optgroup label="US Sizes">
|
||||
<option value="Letter" selected>Letter (8.5" × 11")</option>
|
||||
<option value="Legal">Legal (8.5" × 14")</option>
|
||||
<option value="Tabloid">Tabloid (11" × 17")</option>
|
||||
<option value="Ledger">Ledger (17" × 11")</option>
|
||||
<option value="Executive">Executive (7.25" × 10.5")</option>
|
||||
<option value="Folio">Folio (8.5" × 13")</option>
|
||||
</optgroup>
|
||||
<optgroup label="A Series (ISO 216)">
|
||||
<option value="4A0">4A0 (1682 × 2378 mm)</option>
|
||||
<option value="2A0">2A0 (1189 × 1682 mm)</option>
|
||||
<option value="A0">A0 (841 × 1189 mm)</option>
|
||||
<option value="A1">A1 (594 × 841 mm)</option>
|
||||
<option value="A2">A2 (420 × 594 mm)</option>
|
||||
<option value="A3">A3 (297 × 420 mm)</option>
|
||||
<option value="A4">A4 (210 × 297 mm)</option>
|
||||
<option value="A5">A5 (148 × 210 mm)</option>
|
||||
<option value="A6">A6 (105 × 148 mm)</option>
|
||||
</optgroup>
|
||||
<optgroup label="B Series (ISO 216)">
|
||||
<option value="B0">B0 (1000 × 1414 mm)</option>
|
||||
<option value="B1">B1 (707 × 1000 mm)</option>
|
||||
<option value="B2">B2 (500 × 707 mm)</option>
|
||||
<option value="B3">B3 (353 × 500 mm)</option>
|
||||
<option value="B4">B4 (250 × 353 mm)</option>
|
||||
<option value="B5">B5 (176 × 250 mm)</option>
|
||||
</optgroup>
|
||||
<optgroup label="C Series (Envelopes)">
|
||||
<option value="C0">C0 (917 × 1297 mm)</option>
|
||||
<option value="C1">C1 (648 × 917 mm)</option>
|
||||
<option value="C2">C2 (458 × 648 mm)</option>
|
||||
<option value="C3">C3 (324 × 458 mm)</option>
|
||||
<option value="C4">C4 (229 × 324 mm)</option>
|
||||
<option value="C5">C5 (162 × 229 mm)</option>
|
||||
<option value="C6">C6 (114 × 162 mm)</option>
|
||||
<option value="C7">C7 (81 × 114 mm)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="block mb-3 text-sm font-medium text-white">Grid Mode</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="grid-mode" value="1x2" checked
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>1×2 (Booklet)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="grid-mode" value="2x2"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>2×2</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="grid-mode" value="2x4"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>2×4</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="grid-mode" value="4x4"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>4×4</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="block mt-4 mb-3 text-sm font-medium text-white">Orientation</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="orientation" value="auto" checked
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Auto (best for layout)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="orientation" value="portrait"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Portrait</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-gray-300">
|
||||
<input type="radio" name="orientation" value="landscape"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600">
|
||||
<span>Landscape</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-6">
|
||||
<button id="preview-btn"
|
||||
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-semibold py-3 px-4 rounded-lg transition-colors"
|
||||
disabled>
|
||||
<i data-lucide="eye" class="w-4 h-4 inline-block mr-2"></i>
|
||||
Generate Preview
|
||||
</button>
|
||||
<button id="download-btn" class="flex-1 btn-gradient" disabled
|
||||
data-i18n="tools:pdfBooklet.createBooklet">
|
||||
<i data-lucide="download" class="w-4 h-4 inline-block mr-2"></i>
|
||||
Create Booklet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="booklet-preview"
|
||||
class="bg-gray-900 rounded-lg border border-gray-700 p-4 min-h-[200px] overflow-auto">
|
||||
<p class="text-gray-400 text-center py-8">Upload a PDF and click "Generate Preview" to see the
|
||||
booklet layout</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="tools:pdfBooklet.processing">
|
||||
Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/nicholaschen09/BentoPDF" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-booklet-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
327
src/pages/pdf-layers.html
Normal file
327
src/pages/pdf-layers.html
Normal file
@@ -0,0 +1,327 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF Layers - Manage OCG Layers - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="View, toggle, add, and delete Optional Content Groups (OCG) in your PDF files. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<style>
|
||||
.layers-container {
|
||||
background: #374151;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.layers-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-bottom: 1px solid #4B5563;
|
||||
}
|
||||
|
||||
.layer-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.layer-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layer-toggle input[type="checkbox"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
accent-color: #6366F1;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
color: #E5E7EB;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.layer-locked {
|
||||
font-size: 0.75rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-actions button {
|
||||
background: #4B5563;
|
||||
border: none;
|
||||
color: #9CA3AF;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.layer-actions button:hover {
|
||||
background: #6B7280;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.layer-delete:hover {
|
||||
background: #DC2626 !important;
|
||||
}
|
||||
|
||||
.layers-empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.add-layer-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.add-layer-form input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
background: #1F2937;
|
||||
border: 1px solid #4B5563;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.add-layer-form button {
|
||||
background: #6366F1;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.add-layer-form button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.add-layer-form button:hover {
|
||||
background: #4F46E5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">PDF Layers</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Manage Optional Content Groups (OCG) in your PDF. View, toggle visibility, add new layers, or delete
|
||||
existing ones.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select a PDF</span> or
|
||||
drag and drop</p>
|
||||
<p class="text-xs text-gray-500">Single PDF file only</p>
|
||||
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="process-btn-container" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Load Layers</button>
|
||||
</div>
|
||||
|
||||
<div id="layers-container" class="hidden">
|
||||
<div class="layers-container">
|
||||
<div class="add-layer-form">
|
||||
<input type="text" id="new-layer-name" placeholder="Enter new layer name..." />
|
||||
<button id="add-layer-btn">Add Layer</button>
|
||||
</div>
|
||||
<div id="layers-list" class="layers-list">
|
||||
<div class="layers-empty">
|
||||
<p>Loading layers...</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="save-layers-btn" class="btn-gradient w-full mt-4">Download Modified PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Input Modal -->
|
||||
<div id="input-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="input-title" class="text-xl font-bold text-white mb-2">Add Layer</h3>
|
||||
<p id="input-message" class="text-gray-300 mb-4">Enter name for the new layer:</p>
|
||||
<input type="text" id="input-value"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-4 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Layer Name">
|
||||
<div class="flex justify-end gap-3">
|
||||
<button id="input-cancel"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Cancel</button>
|
||||
<button id="input-confirm"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg transition-colors">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<i data-lucide="github"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-layers-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
173
src/pages/pdf-to-csv.html
Normal file
173
src/pages/pdf-to-csv.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to CSV - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract tables from PDF files and convert to CSV format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-csv.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pdf-to-csv.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pdf-to-csv.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/pdf-to-csv.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-csv.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2"><a href="/">BentoPDF</a></span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pdfToCsv.name">PDF to CSV</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pdfToCsv.subtitle">Extract tables from PDF and convert to CSV
|
||||
format.</p>
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF file</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="options-panel" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Convert to CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub"><svg class="w-6 h-6"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg></a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord"><svg class="w-6 h-6"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg></a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X"><svg
|
||||
class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-csv-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
227
src/pages/pdf-to-docx.html
Normal file
227
src/pages/pdf-to-docx.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to DOCX - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert PDF files to editable Word documents (DOCX). Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-docx.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-docx.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">PDF to DOCX</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert PDF files to editable Word documents. Preserves text, formatting, and layout.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span>
|
||||
<span data-i18n="upload.orDragAndDrop">or drag and drop</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6 space-y-6">
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Convert to DOCX</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-docx-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
173
src/pages/pdf-to-excel.html
Normal file
173
src/pages/pdf-to-excel.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to Excel - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract tables from PDF files and convert to Excel (XLSX) format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-excel.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pdf-to-excel.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pdf-to-excel.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/pdf-to-excel.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-excel.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2"><a href="/">BentoPDF</a></span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pdfToExcel.name">PDF to Excel</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pdfToExcel.subtitle">Extract tables from PDF and convert to
|
||||
Excel (XLSX) format.</p>
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select a file</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF file</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="options-panel" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Convert to Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub"><svg class="w-6 h-6"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg></a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord"><svg class="w-6 h-6"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg></a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X"><svg
|
||||
class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-excel-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
188
src/pages/pdf-to-markdown.html
Normal file
188
src/pages/pdf-to-markdown.html
Normal file
@@ -0,0 +1,188 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to Markdown - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert PDF files to Markdown format. Extract text with optional embedded images. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-markdown.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-markdown.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">PDF to Markdown</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert PDF files to Markdown format. Preserves text structure with optional image embedding.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span>Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6 space-y-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" id="include-images"
|
||||
class="w-4 h-4 rounded bg-gray-700 border-gray-600 text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="include-images" class="text-sm text-gray-300">Include images as base64</label>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Convert to Markdown</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/nickvidal/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<i data-lucide="github"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<i data-lucide="message-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-markdown-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
227
src/pages/pdf-to-pdfa.html
Normal file
227
src/pages/pdf-to-pdfa.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to PDF/A - Convert PDF for Long-term Archiving - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert standard PDF files to PDF/A format (PDF/A-1b, PDF/A-2b, PDF/A-3b) for long-term preservation and archiving using Ghostscript directly in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Convert PDF to PDF/A</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert your PDF documents to PDF/A format for long-term archiving and preservation.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="options-container" class="hidden mt-6 space-y-6">
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-300">PDF/A Version</label>
|
||||
<select id="pdfa-level"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="PDF/A-1b">PDF/A-1b (Strict, no transparency)</option>
|
||||
<option value="PDF/A-2b" selected>PDF/A-2b (Recommended, allows transparency)</option>
|
||||
<option value="PDF/A-3b">PDF/A-3b (Modern, allows attachments)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Convert to PDF/A</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loader Modal -->
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Converting...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Modal -->
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-pdfa-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
218
src/pages/pdf-to-svg.html
Normal file
218
src/pages/pdf-to-svg.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to SVG - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert PDF pages to SVG images. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-svg.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pdf-to-svg.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pdf-to-svg.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-svg.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pdfToSvg.name">PDF to SVG</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pdfToSvg.subtitle">
|
||||
Convert each page of a PDF file into a scalable vector graphic (SVG) for perfect quality at any size.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500">PDF files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="options-panel" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">Convert to SVG</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title"
|
||||
data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pdf-to-svg-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
217
src/pages/pdf-to-text.html
Normal file
217
src/pages/pdf-to-text.html
Normal file
@@ -0,0 +1,217 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PDF to Text - Extract Text from PDF Files - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract text from PDF files and save as plain text (.txt). Free, secure, and runs entirely in your browser." />
|
||||
<meta name="keywords"
|
||||
content="PDF to text, extract text from PDF, PDF text extractor, convert PDF to TXT, free PDF tool" />
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pdf-to-text.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pdf-to-text.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pdf-to-text.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/pdf-to-text.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pdf-to-text.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pdfToText.name">PDF to Text</h1>
|
||||
<p class="text-gray-400 mb-4" data-i18n="tools:pdfToText.subtitle">
|
||||
Extract all text from PDF files and save as plain text (.txt). Supports multiple files.
|
||||
</p>
|
||||
<div class="bg-amber-900/30 border border-amber-700/50 rounded-lg p-3 mb-6">
|
||||
<p class="text-amber-200 text-sm">
|
||||
<strong>Note:</strong> This tool works ONLY with digitally created PDFs. For scanned documents or
|
||||
image-based PDFs, use our <a href="/ocr-pdf.html"
|
||||
class="text-indigo-400 hover:text-indigo-300 underline">OCR PDF tool</a> instead.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="upload-section mb-6">
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-300">
|
||||
<span class="font-semibold" data-i18n="upload.clickToSelect">Click to select files</span>
|
||||
<span data-i18n="upload.orDragAndDrop">or drag and drop</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">PDF files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input type="file" id="file-input" accept="application/pdf" multiple
|
||||
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<div id="extract-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:pdfToText.convertButton">Extract Text</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="status-message" class="mt-4 hidden p-3 rounded-lg text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
|
||||
data-i18n="alert.ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/logic/pdf-to-text-page.ts"></script>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -101,8 +101,8 @@
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="x"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
232
src/pages/powerpoint-to-pdf.html
Normal file
232
src/pages/powerpoint-to-pdf.html
Normal file
@@ -0,0 +1,232 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PowerPoint to PDF - Convert PPTX, PPT to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert PowerPoint presentations (PPTX, PPT, ODP) to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/powerpoint-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/powerpoint-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/powerpoint-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/powerpoint-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:powerpointToPdf.name">PowerPoint to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:powerpointToPdf.subtitle">
|
||||
Convert PowerPoint presentations (PPTX, PPT, ODP) to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold"
|
||||
data-i18n="upload.clickToSelect">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:powerpointToPdf.acceptedFormats">PPTX, PPT, ODP
|
||||
files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".ppt,.pptx,.odp,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.oasis.opendocument.presentation"
|
||||
multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearAll">Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full"
|
||||
data-i18n="tools:powerpointToPdf.convertButton">Convert to PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="/licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/powerpoint-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
198
src/pages/prepare-pdf-for-ai.html
Normal file
198
src/pages/prepare-pdf-for-ai.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Prepare PDF for AI - Extract for LLM/RAG - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Extract PDF content as AI-ready JSON for LlamaIndex, LangChain, and other LLM frameworks. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Prepare PDF for AI</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Extract PDF content as LlamaIndex-compatible JSON documents. Perfect for RAG pipelines, LangChain, and
|
||||
other LLM frameworks.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> or
|
||||
drag and drop</p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span>Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="extract-options" class="hidden mt-6 space-y-6">
|
||||
<div class="bg-gray-700 rounded-lg p-4 text-sm text-gray-300">
|
||||
<p class="font-medium text-white mb-2">Output Format:</p>
|
||||
<p>Each PDF will be extracted as a JSON file containing an array of LlamaIndex Document objects
|
||||
with:</p>
|
||||
<ul class="list-disc list-inside mt-2 space-y-1 text-gray-400">
|
||||
<li><code class="text-indigo-400">text</code> - Extracted text content per page</li>
|
||||
<li><code class="text-indigo-400">metadata</code> - Page number, headings, and document info
|
||||
</li>
|
||||
<li><code class="text-indigo-400">extra_info</code> - Additional context for RAG systems</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Extract for AI</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<i data-lucide="github"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/prepare-pdf-for-ai-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
198
src/pages/psd-to-pdf.html
Normal file
198
src/pages/psd-to-pdf.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PSD to PDF - Convert Photoshop Files to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert Adobe Photoshop (PSD) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/psd-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/psd-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/psd-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/psd-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/psd-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:psdToPdf.name">PSD to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:psdToPdf.subtitle">
|
||||
Convert Adobe Photoshop (PSD) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:psdToPdf.acceptedFormats">PSD files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".psd" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6 hidden">
|
||||
<span data-i18n="tools:psdToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/psd-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
208
src/pages/pub-to-pdf.html
Normal file
208
src/pages/pub-to-pdf.html
Normal file
@@ -0,0 +1,208 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PUB to PDF - Convert Microsoft Publisher to PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert Microsoft Publisher (PUB) files to PDF format. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link rel="alternate" hreflang="en" href="/en/pub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="de" href="/de/pub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="vi" href="/vi/pub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="zh" href="/zh/pub-to-pdf.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="/en/pub-to-pdf.html" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.home">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2" data-i18n="tools:pubToPdf.name">PUB to PDF</h1>
|
||||
<p class="text-gray-400 mb-6" data-i18n="tools:pubToPdf.subtitle">
|
||||
Convert Microsoft Publisher (PUB) files to PDF format. Supports multiple files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> <span
|
||||
data-i18n="upload.orDragAndDrop">or drag and drop</span></p>
|
||||
<p class="text-xs text-gray-500" data-i18n="tools:pubToPdf.acceptedFormats">PUB files</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">Your files never leave your
|
||||
device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".pub" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span data-i18n="upload.addMore">Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span data-i18n="upload.clearFiles">Clear Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="convert-options" class="hidden mt-6">
|
||||
<button id="process-btn" class="btn-gradient w-full">
|
||||
<span data-i18n="tools:pubToPdf.convertButton">Convert to PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs mt-6">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0"></i>
|
||||
<p data-i18n="tools.firstLoadNotice">
|
||||
First load takes a moment as we download our conversion engine. After that, all loads will be
|
||||
instant.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2" data-i18n="alert.title">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
||||
<p class="text-gray-500 text-xs mt-2">Version <span id="app-version"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/Bgq3Ay3f2w" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400"
|
||||
title="X (Twitter)">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/pub-to-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
216
src/pages/rasterize-pdf.html
Normal file
216
src/pages/rasterize-pdf.html
Normal file
@@ -0,0 +1,216 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rasterize PDF - Convert to Image-Based PDF - BentoPDF</title>
|
||||
<meta name="description"
|
||||
content="Convert PDF to image-based PDF. Flatten layers, remove selectable text. Free, secure, and runs entirely in your browser.">
|
||||
<link rel="icon" type="image/png" href="/images/favicon.svg" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
<a href="/about.html" class="nav-link">About</a>
|
||||
<a href="/contact.html" class="nav-link">Contact</a>
|
||||
<a href="/" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center">
|
||||
<button id="mobile-menu-button" type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||
<a href="/" class="mobile-nav-link">Home</a>
|
||||
<a href="/about.html" class="mobile-nav-link">About</a>
|
||||
<a href="/contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="uploader" class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900">
|
||||
<div id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||
<button id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer">Back to Tools</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Rasterize PDF</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Convert vector graphics and text to images. Useful for flattening layers, removing selectable text, or
|
||||
creating print-ready files.
|
||||
</p>
|
||||
|
||||
<div id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 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>
|
||||
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select files</span> or
|
||||
drag and drop</p>
|
||||
<p class="text-xs text-gray-500">One or more PDF files</p>
|
||||
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||
</div>
|
||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf" multiple>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i data-lucide="plus"></i> <span>Add More Files</span>
|
||||
</button>
|
||||
<button id="clear-files-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="trash-2"></i> <span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="rasterize-options" class="hidden mt-6 space-y-6">
|
||||
<div>
|
||||
<label for="rasterize-dpi" class="block mb-2 text-sm font-medium text-gray-300">DPI
|
||||
(Resolution)</label>
|
||||
<select id="rasterize-dpi"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="72">72 (Screen)</option>
|
||||
<option value="150" selected>150 (Default)</option>
|
||||
<option value="200">200 (Good)</option>
|
||||
<option value="300">300 (Print)</option>
|
||||
<option value="600">600 (High Quality)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="rasterize-format" class="block mb-2 text-sm font-medium text-gray-300">Image
|
||||
Format</label>
|
||||
<select id="rasterize-format"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="png" selected>PNG (Lossless)</option>
|
||||
<option value="jpeg">JPEG (Smaller file size)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="rasterize-grayscale"
|
||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<label for="rasterize-grayscale" class="ml-2 text-sm font-medium text-gray-300">Convert to
|
||||
Grayscale</label>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">Rasterize PDF</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/about.html" class="hover:text-indigo-400">About Us</a></li>
|
||||
<li><a href="/faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||
<li><a href="/contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li><a href="/licensing.html" class="hover:text-indigo-400">Licensing</a></li>
|
||||
<li><a href="/terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
||||
<li><a href="/privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||
<i data-lucide="github"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/rasterize-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user