{"id":4600,"date":"2025-01-01T21:09:57","date_gmt":"2025-01-01T15:39:57","guid":{"rendered":"https:\/\/rajmedical.co.in\/?p=4600"},"modified":"2026-06-21T21:09:58","modified_gmt":"2026-06-21T15:39:58","slug":"image-to-pdf-converter","status":"publish","type":"post","link":"https:\/\/rajmedical.co.in\/hi\/image-to-pdf-converter\/","title":{"rendered":"Image to PDF Converter"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n<title>Contact \u2014 image \u21c4 PDF converter<\/title>\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Big+Shoulders+Display:wght@600;700;800&#038;family=IBM+Plex+Mono:wght@400;500;600&#038;family=IBM+Plex+Sans:wght@400;500;600&#038;display=swap\" rel=\"stylesheet\">\n<style>\n\n  :root{\n    --bg: #1c1b19;\n    --panel: #242220;\n    --panel-2: #2b2925;\n    --border: #3a3733;\n    --paper: #f2ede4;\n    --paper-dim: #c9c2b4;\n    --steel: #8fa3a8;\n    --steel-dim: #6c7a7e;\n    --safelight: #ff6a2b;\n    --safelight-dim: #c9531f;\n    --good: #6fcf7a;\n    --warn: #ffb84d;\n    --radius: 4px;\n  }\n  *{ box-sizing: border-box; }\n  html,body{ margin:0; padding:0; }\n  .wrap{\n    background: var(--bg);\n    color: var(--paper);\n    font-family: 'IBM Plex Sans', sans-serif;\n    -webkit-font-smoothing: antialiased;\n    min-height: 100vh;\n  }\n  .mono{ font-family: 'IBM Plex Mono', monospace; }\n  a{ color: inherit; }\n  button{ font-family: inherit; }\n  ::selection{ background: var(--safelight); color: #1c1b19; }\n\n  :focus-visible{ outline: 2px solid var(--safelight); outline-offset: 2px; }\n\n  .wrap{\n    max-width: 980px;\n    margin: 0 auto;\n    padding: 56px 24px 80px;\n  }\n\n  \/* ---------- Header ---------- *\/\n  header{ margin-bottom: 40px; }\n  .eyebrow{\n    font-size: 11px;\n    letter-spacing: 0.16em;\n    text-transform: uppercase;\n    color: var(--steel);\n    margin: 0 0 10px;\n  }\n  .eyebrow .dot{ color: var(--safelight); }\n  h1.wordmark{\n    font-family: 'Big Shoulders Display', sans-serif;\n    font-weight: 800;\n    font-size: clamp(56px, 11vw, 96px);\n    line-height: 0.86;\n    letter-spacing: -0.01em;\n    margin: 0 0 16px;\n    color: var(--paper);\n    opacity: 0;\n    transform: translateY(10px);\n    animation: rise 0.6s ease forwards;\n  }\n  h1.wordmark span{ color: var(--safelight); }\n  @keyframes rise{ to{ opacity:1; transform: translateY(0); } }\n  .tagline{\n    font-size: 16px;\n    line-height: 1.55;\n    color: var(--paper-dim);\n    max-width: 52ch;\n    margin: 0;\n  }\n\n  \/* ---------- Dropzone ---------- *\/\n  .dropzone{\n    position: relative;\n    border: 2px dashed var(--border);\n    border-radius: var(--radius);\n    padding: 64px 24px;\n    text-align: center;\n    cursor: pointer;\n    transition: border-color 0.15s ease, background-color 0.15s ease;\n    margin-bottom: 28px;\n  }\n  .dropzone:hover{ border-color: #545049; }\n  .dropzone.active{\n    border-color: var(--safelight);\n    background: rgba(255,106,43,0.06);\n  }\n  .dropzone .corner{\n    position: absolute;\n    width: 18px; height: 18px;\n    border: 0 solid var(--safelight);\n    opacity: 0.85;\n  }\n  .dropzone .corner.tl{ top:10px; left:10px; border-top-width:2px; border-left-width:2px; }\n  .dropzone .corner.tr{ top:10px; right:10px; border-top-width:2px; border-right-width:2px; }\n  .dropzone .corner.bl{ bottom:10px; left:10px; border-bottom-width:2px; border-left-width:2px; }\n  .dropzone .corner.br{ bottom:10px; right:10px; border-bottom-width:2px; border-right-width:2px; }\n  .dropzone svg{ color: var(--steel); margin-bottom: 14px; }\n  .dropzone .dz-title{ font-size: 19px; font-weight: 500; margin: 0 0 8px; color: var(--paper); }\n  .dropzone .dz-sub{ font-size: 12px; color: var(--steel); letter-spacing: 0.02em; }\n  .dropzone input[type=file]{ display:none; }\n\n  \/* ---------- Global stats \/ controls bar ---------- *\/\n  .statsbar{\n    display:none;\n    background: var(--panel);\n    border: 1px solid var(--border);\n    border-radius: var(--radius);\n    padding: 14px 18px;\n    margin-bottom: 20px;\n    align-items: center;\n    justify-content: space-between;\n    gap: 16px;\n    flex-wrap: wrap;\n  }\n  .statsbar.show{ display:flex; }\n  .statsbar .totals{ font-size: 13px; color: var(--paper-dim); }\n  .statsbar .totals b{ color: var(--paper); font-weight: 600; }\n  .statsbar .totals .pct{ color: var(--good); font-weight: 600; }\n  .statsbar .actions{ display:flex; gap:8px; flex-wrap: wrap; align-items:center; }\n\n  select.mini, .num-input{\n    background: var(--panel-2);\n    border: 1px solid var(--border);\n    color: var(--paper);\n    font-family: 'IBM Plex Mono', monospace;\n    font-size: 12px;\n    padding: 6px 8px;\n    border-radius: var(--radius);\n  }\n\n  .btn{\n    font-family: 'IBM Plex Mono', monospace;\n    font-size: 11px;\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    padding: 8px 14px;\n    border-radius: var(--radius);\n    border: 1px solid var(--border);\n    background: transparent;\n    color: var(--paper-dim);\n    cursor: pointer;\n    transition: all 0.12s ease;\n  }\n  .btn:hover{ border-color: var(--steel); color: var(--paper); }\n  .btn.primary{\n    background: var(--safelight);\n    border-color: var(--safelight);\n    color: #1c1503;\n    font-weight: 600;\n  }\n  .btn.primary:hover{ background: #ff7d45; }\n  .btn:disabled{ opacity:0.35; cursor: not-allowed; }\n\n  \/* ---------- Cards ---------- *\/\n  .cards{ display:flex; flex-direction: column; gap: 16px; }\n  .card{\n    background: var(--panel);\n    border: 1px solid var(--border);\n    border-radius: var(--radius);\n    padding: 18px;\n    position: relative;\n    opacity: 0;\n    transform: translateY(6px);\n    animation: cardin 0.25s ease forwards;\n  }\n  @keyframes cardin{ to{ opacity:1; transform: translateY(0); } }\n  .card.skeleton .card-grid{ opacity: 0.4; }\n  .card.error{ border-color: var(--safelight-dim); }\n  .card .remove{\n    position:absolute; top:12px; right:12px;\n    background:none; border:none; color: var(--steel-dim);\n    font-size: 18px; line-height:1; cursor:pointer; padding: 4px;\n  }\n  .card .remove:hover{ color: var(--safelight); }\n\n  .card-grid{\n    display:grid;\n    grid-template-columns: 150px 1fr;\n    gap: 20px;\n  }\n  @media (max-width: 640px){\n    .card-grid{ grid-template-columns: 1fr; }\n  }\n\n  .thumb-wrap{\n    width:100%;\n    aspect-ratio: 1\/1;\n    border-radius: 2px;\n    border: 1px solid var(--border);\n    overflow:hidden;\n    background-image:\n      linear-gradient(45deg, #2b2925 25%, transparent 25%),\n      linear-gradient(-45deg, #2b2925 25%, transparent 25%),\n      linear-gradient(45deg, transparent 75%, #2b2925 75%),\n      linear-gradient(-45deg, transparent 75%, #2b2925 75%);\n    background-size: 16px 16px;\n    background-position: 0 0, 0 8px, 8px -8px, -8px 0px;\n    background-color: #1f1e1b;\n  }\n  .thumb-wrap img{ width:100%; height:100%; object-fit: contain; display:block; }\n\n  .card-head{ display:flex; align-items:flex-start; justify-content:space-between; gap: 10px; margin-bottom: 14px; padding-right: 20px; }\n  .card-name{ font-size: 13px; color: var(--paper); word-break: break-all; margin: 0 0 6px; }\n  .pill-row{ display:flex; gap:8px; flex-wrap: wrap; align-items:center; }\n  .pill{\n    font-size: 10px;\n    letter-spacing: 0.05em;\n    text-transform: uppercase;\n    border: 1px solid var(--steel-dim);\n    color: var(--steel);\n    padding: 2px 6px;\n    border-radius: 2px;\n  }\n  .meta-text{ font-size: 11px; color: var(--steel); }\n\n  \/* Quality control *\/\n  .ctrl-block{ margin-bottom: 16px; }\n  .ctrl-label{\n    font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;\n    color: var(--steel); display:flex; justify-content: space-between; margin-bottom: 8px;\n  }\n  .ctrl-label .val{ color: var(--paper); }\n  input[type=range]{\n    -webkit-appearance: none;\n    width: 100%;\n    height: 4px;\n    background: var(--border);\n    border-radius: 2px;\n    margin: 0;\n  }\n  input[type=range]::-webkit-slider-thumb{\n    -webkit-appearance: none;\n    width: 15px; height: 15px;\n    border-radius: 50%;\n    background: var(--safelight);\n    cursor: pointer;\n    border: 2px solid #1c1b19;\n    box-shadow: 0 0 0 1px var(--safelight);\n  }\n  input[type=range]::-moz-range-thumb{\n    width: 15px; height: 15px;\n    border-radius: 50%;\n    background: var(--safelight);\n    cursor: pointer;\n    border: 2px solid #1c1b19;\n  }\n  .stops-row{\n    display:flex; justify-content: space-between;\n    margin-top: 6px;\n    font-size: 10px;\n    color: var(--steel-dim);\n    font-family: 'IBM Plex Mono', monospace;\n  }\n  .stops-row span{ flex: 1; text-align:center; }\n  .stops-row span:first-child{ text-align:left; }\n  .stops-row span:last-child{ text-align:right; }\n  .stops-row span.active{ color: var(--safelight); font-weight: 600; }\n\n  \/* Format chips *\/\n  .chip-row{ display:flex; gap:8px; flex-wrap: wrap; }\n  .chip{\n    flex: 1 1 90px;\n    position: relative;\n    border: 1px solid var(--border);\n    background: var(--panel-2);\n    border-radius: var(--radius);\n    padding: 8px 10px;\n    text-align:left;\n    cursor: pointer;\n    transition: border-color 0.12s ease, background-color 0.12s ease;\n  }\n  .chip:hover:not(:disabled){ border-color: var(--steel); }\n  .chip.active{ border-color: var(--safelight); background: rgba(255,106,43,0.08); }\n  .chip:disabled{ opacity: 0.32; cursor: not-allowed; }\n  .chip .fmt{ display:block; font-family:'IBM Plex Mono', monospace; font-size: 11px; font-weight:600; letter-spacing:0.04em; color: var(--paper); text-transform: uppercase; }\n  .chip .sz{ display:block; font-family:'IBM Plex Mono', monospace; font-size: 11px; color: var(--steel); margin-top:3px; }\n  .chip.active .fmt{ color: var(--safelight); }\n  .chip .badge{\n    position:absolute; top:-7px; right:6px;\n    font-size: 8px; letter-spacing: 0.05em; text-transform: uppercase;\n    background: var(--good); color: #0e1f10; padding: 1px 4px; border-radius: 2px; font-weight:700;\n  }\n\n  \/* Resize row *\/\n  .resize-row{ display:flex; align-items:center; gap:10px; margin-top: 14px; font-size: 12px; color: var(--paper-dim); flex-wrap: wrap; }\n  .resize-row label.cb{ display:flex; align-items:center; gap:7px; cursor:pointer; }\n  input[type=checkbox]{\n    appearance: none; width:15px; height:15px; border:1px solid var(--steel-dim); border-radius:2px;\n    background: var(--panel-2); cursor:pointer; position: relative; flex-shrink:0;\n  }\n  input[type=checkbox]:checked{ background: var(--safelight); border-color: var(--safelight); }\n  input[type=checkbox]:checked::after{\n    content:''; position:absolute; left:4px; top:1px; width:4px; height:8px;\n    border: solid #1c1b19; border-width: 0 2px 2px 0; transform: rotate(45deg);\n  }\n  .resize-row .num-input{ width: 80px; }\n  .resize-fields{ display:flex; align-items:center; gap:8px; }\n  .resize-fields[hidden]{ display:none; }\n\n  \/* Result row *\/\n  .result-row{\n    margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);\n    display:flex; align-items:center; justify-content: space-between; gap: 14px; flex-wrap: wrap;\n  }\n  .result-figures{ display:flex; align-items:baseline; gap: 12px; flex-wrap: wrap; }\n  .fig{ }\n  .fig .flabel{ font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--steel); display:block; margin-bottom: 3px; }\n  .fig .fval{ font-family: 'IBM Plex Mono', monospace; font-size: 14px; color: var(--steel); }\n  .fig.out .fval{ font-family: 'Big Shoulders Display', sans-serif; font-size: 26px; font-weight: 700; color: var(--paper); }\n  .arrow{ color: var(--steel-dim); font-size: 16px; }\n  .pct-badge{\n    font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 600;\n    padding: 3px 8px; border-radius: 3px;\n  }\n  .pct-badge.down{ color: var(--good); background: rgba(111,207,122,0.12); }\n  .pct-badge.up{ color: var(--warn); background: rgba(255,184,77,0.12); }\n\n  .err-msg{ font-size: 13px; color: var(--warn); margin: 4px 0 0; }\n\n  \/* ---------- Footer ---------- *\/\n  footer{ margin-top: 56px; text-align:center; }\n  footer p{ font-size: 11px; color: var(--steel-dim); letter-spacing: 0.02em; line-height:1.6; }\n\n  @media (prefers-reduced-motion: reduce){\n    *{ animation: none !important; transition: none !important; }\n  }\n\n  \/* ---------- Tabs ---------- *\/\n  .tabs{ display:flex; gap:8px; margin-bottom: 24px; border-bottom: 1px solid var(--border); }\n  .tab{\n    font-family: 'IBM Plex Mono', monospace;\n    font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase;\n    background: none; border: none; color: var(--steel);\n    padding: 12px 4px 14px; margin-right: 22px; cursor: pointer;\n    border-bottom: 2px solid transparent; transform: translateY(1px);\n  }\n  .tab .n{ color: var(--steel-dim); margin-right: 6px; }\n  .tab:hover{ color: var(--paper-dim); }\n  .tab.active{ color: var(--paper); border-bottom-color: var(--safelight); }\n  .tab.active .n{ color: var(--safelight); }\n  .mode-panel{ display:none; }\n  .mode-panel.active{ display:block; }\n\n  \/* ---------- Settings panel ---------- *\/\n  .settings-panel{\n    background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);\n    padding: 18px; margin-bottom: 20px;\n  }\n  .settings-title{ font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--steel); margin: 0 0 14px; }\n  .settings-grid{ display:grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }\n  @media (max-width: 760px){ .settings-grid{ grid-template-columns: repeat(2,1fr); } }\n  .field label{ display:block; font-size: 10px; letter-spacing:0.08em; text-transform: uppercase; color: var(--steel); margin-bottom: 7px; }\n  .field select, .field input[type=text]{\n    width: 100%; background: var(--panel-2); border: 1px solid var(--border); color: var(--paper);\n    font-family: 'IBM Plex Mono', monospace; font-size: 12px; padding: 8px 9px; border-radius: var(--radius);\n  }\n  .field-note{ font-size: 10px; color: var(--steel-dim); margin-top: 6px; }\n\n  \/* ---------- Frame grid (image -> pdf source list) ---------- *\/\n  .frame-grid{ display:grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 12px; margin-bottom: 20px; }\n  .frame{\n    background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;\n    opacity:0; transform: translateY(6px); animation: cardin 0.25s ease forwards;\n  }\n  .frame-thumb{\n    position:relative; aspect-ratio: 1\/1;\n    background-image:\n      linear-gradient(45deg, #2b2925 25%, transparent 25%),\n      linear-gradient(-45deg, #2b2925 25%, transparent 25%),\n      linear-gradient(45deg, transparent 75%, #2b2925 75%),\n      linear-gradient(-45deg, transparent 75%, #2b2925 75%);\n    background-size: 14px 14px;\n    background-position: 0 0, 0 7px, 7px -7px, -7px 0px;\n    background-color: #1f1e1b;\n  }\n  .frame-thumb img{ width:100%; height:100%; object-fit: contain; display:block; }\n  .frame-num{\n    position:absolute; top:6px; left:6px;\n    font-family:'IBM Plex Mono', monospace; font-size: 10px; font-weight:700;\n    background: var(--safelight); color:#1c1503; padding: 1px 5px; border-radius: 2px; letter-spacing: 0.04em;\n  }\n  .frame-meta{ padding: 7px 8px; font-size: 10px; color: var(--steel); font-family:'IBM Plex Mono', monospace; line-height:1.5; }\n  .frame-meta .fname{ color: var(--paper-dim); font-family:'IBM Plex Sans', sans-serif; font-size: 11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }\n  .frame-ctrls{ display:flex; border-top: 1px solid var(--border); }\n  .frame-ctrls button{\n    flex:1; background:none; border:none; color: var(--steel); padding: 6px 0; font-size: 12px; cursor:pointer;\n    border-right: 1px solid var(--border);\n  }\n  .frame-ctrls button:last-child{ border-right:none; }\n  .frame-ctrls button:hover:not(:disabled){ color: var(--safelight); background: var(--panel-2); }\n  .frame-ctrls button:disabled{ opacity: 0.3; cursor: default; }\n\n  \/* ---------- Build result panel (image -> pdf) ---------- *\/\n  .build-panel{\n    background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);\n    padding: 18px; display:flex; align-items:center; justify-content: space-between; gap:16px; flex-wrap: wrap;\n  }\n  .build-status{ font-family:'IBM Plex Mono', monospace; font-size: 12px; color: var(--steel); }\n  .build-status b{ color: var(--paper); }\n  .build-status .pct{ color: var(--good); }\n\n  \/* ---------- PDF source cards (pdf -> image) ---------- *\/\n  .pdf-card{\n    background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);\n    padding: 18px; margin-bottom: 18px;\n    opacity:0; transform: translateY(6px); animation: cardin 0.25s ease forwards;\n  }\n  .pdf-card-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; margin-bottom:16px; }\n  .pdf-card-head .pdf-name{ font-size: 14px; color: var(--paper); margin: 0 0 6px; word-break: break-all; }\n  .pdf-controls{ display:grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 16px; }\n  @media (max-width: 700px){ .pdf-controls{ grid-template-columns: 1fr; } }\n  .range-field input[type=text]{\n    width:100%; background: var(--panel-2); border:1px solid var(--border); color: var(--paper);\n    font-family:'IBM Plex Mono', monospace; font-size:12px; padding: 8px 9px; border-radius: var(--radius);\n  }\n  .progress-line{ font-family:'IBM Plex Mono', monospace; font-size: 11px; color: var(--steel); margin: 4px 0 0; min-height: 14px; }\n  .page-grid{ display:grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 12px; margin-top: 16px; }\n  .page-tile{\n    background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius); overflow:hidden;\n    opacity:0; transform: translateY(6px); animation: cardin 0.25s ease forwards;\n  }\n  .page-tile .frame-thumb{ background-color:#1f1e1b; }\n  .page-tile .page-dl{\n    display:block; width:100%; text-align:center; background:none; border:none; border-top:1px solid var(--border);\n    color: var(--steel); font-size: 11px; padding: 6px 0; cursor:pointer; font-family:'IBM Plex Mono', monospace;\n  }\n  .page-tile .page-dl:hover{ color: var(--safelight); background: var(--panel); }\n  .extract-row{ display:flex; align-items:center; gap:12px; flex-wrap: wrap; }\n<\/style>\n<\/head>\n<body>\n<div class=\"wrap\">\n\n  <header>\n    <p class=\"eyebrow\"><span class=\"dot\">\u25cf<\/span> client-side photo lab<\/p>\n    <h1 class=\"wordmark\">CONTA<span>CT<\/span><\/h1>\n    <p class=\"tagline\">Lay your images down on a contact sheet and pull out a PDF \u2014 or split a PDF back into individual prints. Everything happens here; nothing is saved in our server.<\/p>\n  <\/header>\n\n  <nav class=\"tabs\" role=\"tablist\">\n    <button class=\"tab active\" id=\"tabImg2Pdf\" role=\"tab\" aria-selected=\"true\"><span class=\"n\">01<\/span>Images \u2192 PDF<\/button>\n    <button class=\"tab\" id=\"tabPdf2Img\" role=\"tab\" aria-selected=\"false\"><span class=\"n\">02<\/span>PDF \u2192 Images<\/button>\n  <\/nav>\n\n  <!-- ============ MODE 1: IMAGES -> PDF ============ -->\n  <section class=\"mode-panel active\" id=\"panelImg2Pdf\">\n\n    <section class=\"dropzone\" id=\"dzImages\" tabindex=\"0\" role=\"button\" aria-label=\"Upload images\">\n      <span class=\"corner tl\"><\/span><span class=\"corner tr\"><\/span><span class=\"corner bl\"><\/span><span class=\"corner br\"><\/span>\n      <svg width=\"34\" height=\"34\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\">\n        <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/>\n        <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"\/>\n        <path d=\"M21 15l-5-5L5 21\"\/>\n      <\/svg>\n      <p class=\"dz-title\">Drop images here to build a sheet, or click to browse<\/p>\n      <p class=\"dz-sub mono\">JPEG \u00b7 PNG \u00b7 WEBP \u00b7 GIF \u00b7 BMP \u00b7 SVG \u2014 combined into one PDF, in the order shown below<\/p>\n      <input type=\"file\" id=\"imgInput\" accept=\"image\/*\" multiple \/>\n    <\/section>\n\n    <div id=\"img2pdfBody\" style=\"display:none;\">\n      <div class=\"settings-panel\">\n        <p class=\"settings-title\">Sheet settings<\/p>\n        <div class=\"settings-grid\">\n          <div class=\"field\">\n            <label for=\"pageSize\">Page size<\/label>\n            <select id=\"pageSize\">\n              <option value=\"a4\">A4<\/option>\n              <option value=\"letter\">Letter<\/option>\n              <option value=\"legal\">Legal<\/option>\n              <option value=\"fit\">Fit to image<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"field\" id=\"orientationField\">\n            <label for=\"orientation\">Orientation<\/label>\n            <select id=\"orientation\">\n              <option value=\"auto\">Auto (per image)<\/option>\n              <option value=\"portrait\">Portrait<\/option>\n              <option value=\"landscape\">Landscape<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"field\" id=\"marginField\">\n            <label for=\"margin\">Margin<\/label>\n            <select id=\"margin\">\n              <option value=\"0\">None<\/option>\n              <option value=\"18\">Small<\/option>\n              <option value=\"36\" selected>Normal<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"field\">\n            <label>Embed as<\/label>\n            <div class=\"chip-row\" id=\"embedChips\">\n              <button class=\"chip active\" data-embed=\"jpeg\"><span class=\"fmt\">JPEG<\/span><span class=\"sz mono\">compressed<\/span><\/button>\n              <button class=\"chip\" data-embed=\"png\"><span class=\"fmt\">PNG<\/span><span class=\"sz mono\">lossless<\/span><\/button>\n            <\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"ctrl-block\" id=\"embedQualityBlock\" style=\"margin-top:16px;\">\n          <div class=\"ctrl-label\"><span>Compression<\/span><span class=\"val mono\" id=\"embedQLabel\">f\/5.6 \u00b7 75% \u00b7 Balanced<\/span><\/div>\n          <input type=\"range\" min=\"0\" max=\"5\" step=\"1\" value=\"2\" id=\"embedQuality\" \/>\n          <div class=\"stops-row\" id=\"embedStops\"><\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"frame-grid\" id=\"frameGrid\"><\/div>\n\n      <div class=\"build-panel\">\n        <div class=\"build-status mono\" id=\"buildStatus\">Building sheet\u2026<\/div>\n        <button class=\"btn primary\" id=\"downloadPdfBtn\" disabled>Download PDF<\/button>\n      <\/div>\n    <\/div>\n  <\/section>\n\n  <!-- ============ MODE 2: PDF -> IMAGES ============ -->\n  <section class=\"mode-panel\" id=\"panelPdf2Img\">\n\n    <section class=\"dropzone\" id=\"dzPdf\" tabindex=\"0\" role=\"button\" aria-label=\"Upload PDF\">\n      <span class=\"corner tl\"><\/span><span class=\"corner tr\"><\/span><span class=\"corner bl\"><\/span><span class=\"corner br\"><\/span>\n      <svg width=\"34\" height=\"34\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\">\n        <path d=\"M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z\"\/>\n        <path d=\"M14 2v6h6\"\/>\n      <\/svg>\n      <p class=\"dz-title\">Drop a PDF here to pull its pages out, or click to browse<\/p>\n      <p class=\"dz-sub mono\">Pages become individual PNG, JPEG or WEBP frames<\/p>\n      <input type=\"file\" id=\"pdfInput\" accept=\"application\/pdf\" multiple \/>\n    <\/section>\n\n    <div id=\"pdfCards\"><\/div>\n  <\/section>\n\n  <footer>\n    <p><!-- Conversion runs on-device using Canvas, pdf.js and jsPDF.<br>  --> No file ever saves in our server.<\/p>\n  <\/footer>\n\n<\/div>\n\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\"><\/script>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js\"><\/script>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/2.16.105\/pdf.min.js\"><\/script>\n<script>\n(function(){\n  \"use strict\";\n\n  \/\/ ---------- Shared config ----------\n  var QUALITY_STOPS = [\n    { q: 95, label: 'f\/2.8', desc: 'Best quality' },\n    { q: 85, label: 'f\/4',   desc: 'High quality' },\n    { q: 75, label: 'f\/5.6', desc: 'Balanced' },\n    { q: 60, label: 'f\/8',   desc: 'Standard' },\n    { q: 40, label: 'f\/11',  desc: 'Aggressive' },\n    { q: 20, label: 'f\/16',  desc: 'Max compression' }\n  ];\n  var RES_STOPS = [\n    { scale: 1, label: '1\u00d7', dpi: '72 dpi' },\n    { scale: 2, label: '2\u00d7', dpi: '144 dpi' },\n    { scale: 3, label: '3\u00d7', dpi: '216 dpi' },\n    { scale: 4, label: '4\u00d7', dpi: '288 dpi' }\n  ];\n  var MIME = { jpeg: 'image\/jpeg', png: 'image\/png', webp: 'image\/webp' };\n  var EXT  = { jpeg: 'jpg', png: 'png', webp: 'webp' };\n  var PAGE_SIZES = { a4: { w: 595.28, h: 841.89 }, letter: { w: 612, h: 792 }, legal: { w: 612, h: 1008 } };\n  var SUPPORTED = { jpeg: true, png: true, webp: false };\n\n  \/\/ ---------- Utilities ----------\n  function formatBytes(bytes){\n    if (bytes == null) return '\u2014';\n    if (bytes < 1024) return bytes + ' B';\n    var units = ['KB','MB','GB'];\n    var val = bytes, i = -1;\n    do { val \/= 1024; i++; } while (val >= 1024 && i < units.length - 1);\n    return val.toFixed(val < 10 ? 2 : 1) + ' ' + units[i];\n  }\n  function uid(){ return 'x' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); }\n  function baseName(filename){ var i = filename.lastIndexOf('.'); return i > 0 ? filename.slice(0, i) : filename; }\n  function escapeHtml(s){ var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }\n  function pad2(n){ return n < 10 ? '0' + n : '' + n; }\n  function triggerDownload(url, filename){\n    var a = document.createElement('a');\n    a.href = url; a.download = filename;\n    document.body.appendChild(a); a.click(); a.remove();\n  }\n  function detectFormatSupport(mime){\n    return new Promise(function(resolve){\n      try{\n        var c = document.createElement('canvas'); c.width = 2; c.height = 2;\n        c.toBlob(function(blob){ resolve(!!blob &#038;&#038; blob.type === mime); }, mime);\n      } catch(e){ resolve(false); }\n    });\n  }\n\n  \/\/ ===================================================================\n  \/\/ MODE 1: IMAGES -> PDF\n  \/\/ ===================================================================\n  var frames = [];\n  var img2pdf = {\n    settings: { pageSize: 'a4', orientation: 'auto', margin: 36, embed: 'jpeg', qIndex: 2 },\n    pdfURL: null,\n    debounceTimer: null\n  };\n\n  function addImageFrame(file){\n    var id = uid();\n    var frame = { id: id, file: file, name: file.name, img: null, naturalWidth: 0, naturalHeight: 0, objectURL: URL.createObjectURL(file) };\n    frames.push(frame);\n    document.getElementById('img2pdfBody').style.display = '';\n    renderFrameGrid();\n\n    var im = new Image();\n    im.onload = function(){\n      frame.img = im;\n      frame.naturalWidth = im.naturalWidth;\n      frame.naturalHeight = im.naturalHeight;\n      renderFrameGrid();\n      scheduleRebuildPdf(150);\n    };\n    im.onerror = function(){\n      frames = frames.filter(function(f){ return f.id !== id; });\n      renderFrameGrid();\n    };\n    im.src = frame.objectURL;\n  }\n\n  function renderFrameGrid(){\n    var grid = document.getElementById('frameGrid');\n    grid.innerHTML = frames.map(function(f, idx){\n      var dims = f.naturalWidth ? (f.naturalWidth + '\u00d7' + f.naturalHeight) : 'reading\u2026';\n      return (\n        '<div class=\"frame\" data-id=\"' + f.id + '\">' +\n          '<div class=\"frame-thumb\"><span class=\"frame-num mono\">' + pad2(idx + 1) + '<\/span><img decoding=\"async\" src=\"' + f.objectURL + '\" alt=\"\" \/><\/div>' +\n          '<div class=\"frame-meta\"><div class=\"fname\">' + escapeHtml(f.name) + '<\/div>' + dims + '<\/div>' +\n          '<div class=\"frame-ctrls\">' +\n            '<button data-action=\"left\" ' + (idx === 0 ? 'disabled' : '') + '>\u2039<\/button>' +\n            '<button data-action=\"right\" ' + (idx === frames.length - 1 ? 'disabled' : '') + '>\u203a<\/button>' +\n            '<button data-action=\"remove\">\u00d7<\/button>' +\n          '<\/div>' +\n        '<\/div>'\n      );\n    }).join('');\n  }\n\n  function moveFrame(id, dir){\n    var i = frames.findIndex(function(f){ return f.id === id; });\n    if (i < 0) return;\n    var j = i + dir;\n    if (j < 0 || j >= frames.length) return;\n    var tmp = frames[i]; frames[i] = frames[j]; frames[j] = tmp;\n    renderFrameGrid();\n    scheduleRebuildPdf(0);\n  }\n\n  function removeFrame(id){\n    var f = frames.find(function(x){ return x.id === id; });\n    if (f && f.objectURL) URL.revokeObjectURL(f.objectURL);\n    frames = frames.filter(function(x){ return x.id !== id; });\n    renderFrameGrid();\n    if (frames.length === 0){\n      document.getElementById('img2pdfBody').style.display = 'none';\n      document.getElementById('downloadPdfBtn').disabled = true;\n      if (img2pdf.pdfURL){ URL.revokeObjectURL(img2pdf.pdfURL); img2pdf.pdfURL = null; }\n      document.getElementById('buildStatus').textContent = 'No images yet';\n      return;\n    }\n    scheduleRebuildPdf(0);\n  }\n\n  function scheduleRebuildPdf(delay){\n    clearTimeout(img2pdf.debounceTimer);\n    document.getElementById('buildStatus').textContent = 'Building sheet\u2026';\n    img2pdf.debounceTimer = setTimeout(buildPdf, delay);\n  }\n\n  function getPageDims(frame){\n    var s = img2pdf.settings;\n    if (s.pageSize === 'fit'){\n      return { w: frame.naturalWidth, h: frame.naturalHeight, unit: 'px', margin: 0 };\n    }\n    var base = PAGE_SIZES[s.pageSize];\n    var orient = s.orientation;\n    if (orient === 'auto') orient = (frame.naturalWidth > frame.naturalHeight) ? 'landscape' : 'portrait';\n    var w, h;\n    if (orient === 'landscape'){ w = Math.max(base.w, base.h); h = Math.min(base.w, base.h); }\n    else { w = Math.min(base.w, base.h); h = Math.max(base.w, base.h); }\n    return { w: w, h: h, unit: 'pt', margin: parseInt(s.margin, 10) || 0 };\n  }\n\n  function rasterizeForPdf(frame){\n    var canvas = document.createElement('canvas');\n    canvas.width = frame.naturalWidth; canvas.height = frame.naturalHeight;\n    var ctx = canvas.getContext('2d');\n    var fmt = img2pdf.settings.embed;\n    if (fmt === 'jpeg'){ ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); }\n    ctx.drawImage(frame.img, 0, 0);\n    if (fmt === 'jpeg'){\n      var q = QUALITY_STOPS[img2pdf.settings.qIndex].q \/ 100;\n      return canvas.toDataURL('image\/jpeg', q);\n    }\n    return canvas.toDataURL('image\/png');\n  }\n\n  function buildPdf(){\n    var statusEl = document.getElementById('buildStatus');\n    var dlBtn = document.getElementById('downloadPdfBtn');\n    if (frames.length === 0 || frames.some(function(f){ return !f.img; })){\n      if (frames.length === 0) statusEl.textContent = 'No images yet';\n      return;\n    }\n    if (typeof window.jspdf === 'undefined'){\n      statusEl.textContent = 'PDF library failed to load \u2014 check your connection.';\n      return;\n    }\n    try{\n      var doc = null;\n      var fmt = img2pdf.settings.embed;\n      frames.forEach(function(frame, idx){\n        var dims = getPageDims(frame);\n        var dataURL = rasterizeForPdf(frame);\n        var availW = dims.w - dims.margin * 2;\n        var availH = dims.h - dims.margin * 2;\n        var scale = Math.min(availW \/ frame.naturalWidth, availH \/ frame.naturalHeight);\n        var drawW = frame.naturalWidth * scale;\n        var drawH = frame.naturalHeight * scale;\n        var x = (dims.w - drawW) \/ 2;\n        var y = (dims.h - drawH) \/ 2;\n        var orientArg = dims.w > dims.h ? 'l' : 'p';\n        if (idx === 0){\n          doc = new window.jspdf.jsPDF({ orientation: orientArg, unit: dims.unit, format: [dims.w, dims.h] });\n        } else {\n          doc.addPage([dims.w, dims.h], orientArg);\n        }\n        doc.addImage(dataURL, fmt === 'jpeg' ? 'JPEG' : 'PNG', x, y, drawW, drawH);\n      });\n      var blob = doc.output('blob');\n      if (img2pdf.pdfURL) URL.revokeObjectURL(img2pdf.pdfURL);\n      img2pdf.pdfURL = URL.createObjectURL(blob);\n      statusEl.innerHTML = '<b>' + frames.length + (frames.length === 1 ? ' page' : ' pages') + '<\/b> \u00b7 ' + formatBytes(blob.size);\n      dlBtn.disabled = false;\n    } catch(e){\n      statusEl.textContent = 'Could not build the PDF: ' + e.message;\n    }\n  }\n\n  function wireImg2Pdf(){\n    var dz = document.getElementById('dzImages');\n    var input = document.getElementById('imgInput');\n    dz.addEventListener('click', function(){ input.click(); });\n    dz.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); input.click(); } });\n    input.addEventListener('change', function(){\n      Array.from(input.files).filter(function(f){ return f.type.indexOf('image\/') === 0; }).forEach(addImageFrame);\n      input.value = '';\n    });\n    ['dragenter','dragover'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.add('active'); }); });\n    ['dragleave','drop'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.remove('active'); }); });\n    dz.addEventListener('drop', function(e){\n      if (e.dataTransfer && e.dataTransfer.files){\n        Array.from(e.dataTransfer.files).filter(function(f){ return f.type.indexOf('image\/') === 0; }).forEach(addImageFrame);\n      }\n    });\n\n    document.getElementById('frameGrid').addEventListener('click', function(e){\n      var btn = e.target.closest('button[data-action]');\n      if (!btn) return;\n      var frameEl = e.target.closest('.frame');\n      var id = frameEl.dataset.id;\n      var action = btn.dataset.action;\n      if (action === 'left') moveFrame(id, -1);\n      else if (action === 'right') moveFrame(id, 1);\n      else if (action === 'remove') removeFrame(id);\n    });\n\n    document.getElementById('pageSize').addEventListener('change', function(e){\n      img2pdf.settings.pageSize = e.target.value;\n      var isFit = img2pdf.settings.pageSize === 'fit';\n      document.getElementById('orientationField').style.opacity = isFit ? 0.4 : 1;\n      document.getElementById('orientation').disabled = isFit;\n      document.getElementById('marginField').style.opacity = isFit ? 0.4 : 1;\n      document.getElementById('margin').disabled = isFit;\n      scheduleRebuildPdf(0);\n    });\n    document.getElementById('orientation').addEventListener('change', function(e){\n      img2pdf.settings.orientation = e.target.value;\n      scheduleRebuildPdf(0);\n    });\n    document.getElementById('margin').addEventListener('change', function(e){\n      img2pdf.settings.margin = e.target.value;\n      scheduleRebuildPdf(0);\n    });\n\n    var embedStops = document.getElementById('embedStops');\n    embedStops.innerHTML = QUALITY_STOPS.map(function(s, i){ return '<span data-i=\"' + i + '\">' + s.label + '<\/span>'; }).join('');\n    function updateEmbedQLabel(){\n      var s = QUALITY_STOPS[img2pdf.settings.qIndex];\n      document.getElementById('embedQLabel').textContent = s.label + ' \u00b7 ' + s.q + '% \u00b7 ' + s.desc;\n      embedStops.querySelectorAll('span').forEach(function(sp){ sp.classList.toggle('active', parseInt(sp.dataset.i, 10) === img2pdf.settings.qIndex); });\n    }\n    updateEmbedQLabel();\n    document.getElementById('embedQuality').addEventListener('input', function(e){\n      img2pdf.settings.qIndex = parseInt(e.target.value, 10);\n      updateEmbedQLabel();\n      scheduleRebuildPdf(300);\n    });\n\n    document.querySelectorAll('#embedChips .chip').forEach(function(chip){\n      chip.addEventListener('click', function(){\n        document.querySelectorAll('#embedChips .chip').forEach(function(c){ c.classList.remove('active'); });\n        chip.classList.add('active');\n        img2pdf.settings.embed = chip.dataset.embed;\n        var qBlock = document.getElementById('embedQualityBlock');\n        qBlock.style.opacity = img2pdf.settings.embed === 'png' ? 0.4 : 1;\n        document.getElementById('embedQuality').disabled = img2pdf.settings.embed === 'png';\n        scheduleRebuildPdf(0);\n      });\n    });\n\n    document.getElementById('downloadPdfBtn').addEventListener('click', function(){\n      if (!img2pdf.pdfURL) return;\n      triggerDownload(img2pdf.pdfURL, 'contact-sheet.pdf');\n    });\n  }\n\n  \/\/ ===================================================================\n  \/\/ MODE 2: PDF -> IMAGES\n  \/\/ ===================================================================\n  var pdfCards = new Map();\n\n  function parsePageRange(text, maxPages){\n    text = (text || '').trim();\n    if (!text) {\n      var all = [];\n      for (var i = 1; i <= maxPages; i++) all.push(i);\n      return all;\n    }\n    var set = {};\n    text.split(',').forEach(function(tok){\n      tok = tok.trim();\n      if (!tok) return;\n      if (tok.indexOf('-') > -1){\n        var parts = tok.split('-');\n        var a = parseInt(parts[0], 10), b = parseInt(parts[1], 10);\n        if (isNaN(a) || isNaN(b)) return;\n        if (a > b){ var t = a; a = b; b = t; }\n        for (var n = Math.max(1, a); n <= Math.min(maxPages, b); n++) set[n] = true;\n      } else {\n        var v = parseInt(tok, 10);\n        if (!isNaN(v) &#038;&#038; v >= 1 && v <= maxPages) set[v] = true;\n      }\n    });\n    return Object.keys(set).map(Number).sort(function(a, b){ return a - b; });\n  }\n\n  function renderPdfPageToBlob(pdfDoc, pageNum, scale, format, quality){\n    return pdfDoc.getPage(pageNum).then(function(page){\n      var viewport = page.getViewport({ scale: scale });\n      var canvas = document.createElement('canvas');\n      canvas.width = Math.ceil(viewport.width);\n      canvas.height = Math.ceil(viewport.height);\n      var ctx = canvas.getContext('2d');\n      if (format === 'jpeg'){ ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); }\n      return page.render({ canvasContext: ctx, viewport: viewport }).promise.then(function(){\n        return new Promise(function(resolve, reject){\n          var mime = MIME[format];\n          var q = format === 'png' ? undefined : (quality \/ 100);\n          canvas.toBlob(function(blob){ if (blob) resolve(blob); else reject(new Error('encode failed')); }, mime, q);\n        });\n      });\n    });\n  }\n\n  function addPdfFile(file){\n    var id = uid();\n    var card = {\n      id: id, file: file, name: file.name, base: baseName(file.name), size: file.size,\n      pdfDoc: null, numPages: 0,\n      settings: { format: 'jpeg', qIndex: 2, resIndex: 1, range: '' },\n      pageBlobs: {}, el: null, errored: false, extracting: false,\n      previewTimer: null\n    };\n    pdfCards.set(id, card);\n\n    var el = document.createElement('div');\n    el.className = 'pdf-card';\n    el.dataset.id = id;\n    el.innerHTML =\n      '<button class=\"remove\" aria-label=\"Remove\" data-action=\"remove\" style=\"position:static; float:right;\">\u00d7<\/button>' +\n      '<p class=\"pdf-name\">' + escapeHtml(file.name) + '<\/p>' +\n      '<p class=\"meta-text mono\">Reading PDF\u2026<\/p>';\n    document.getElementById('pdfCards').appendChild(el);\n    card.el = el;\n    el.querySelector('[data-action=\"remove\"]').addEventListener('click', function(){ removePdfCard(id); });\n\n    file.arrayBuffer().then(function(buf){\n      if (typeof pdfjsLib === 'undefined') throw new Error('pdf.js failed to load');\n      return pdfjsLib.getDocument({ data: buf }).promise;\n    }).then(function(pdfDoc){\n      card.pdfDoc = pdfDoc;\n      card.numPages = pdfDoc.numPages;\n      buildPdfCardBody(card);\n      computePagePreviewSizes(card);\n    }).catch(function(err){\n      card.errored = true;\n      el.innerHTML =\n        '<button class=\"remove\" aria-label=\"Remove\" data-action=\"remove\" style=\"position:static; float:right;\">\u00d7<\/button>' +\n        '<p class=\"pdf-name\">' + escapeHtml(file.name) + '<\/p>' +\n        '<p class=\"err-msg\">Couldn\\'t open this PDF \u2014 it may be encrypted, corrupted, or not a valid PDF file.<\/p>';\n      el.querySelector('[data-action=\"remove\"]').addEventListener('click', function(){ removePdfCard(id); });\n    });\n  }\n\n  function buildPdfCardBody(card){\n    var el = card.el;\n    var chips = ['jpeg','png','webp'].map(function(fmt){\n      var supported = SUPPORTED[fmt];\n      return '<button class=\"chip' + (fmt === card.settings.format ? ' active' : '') + '\" data-fmt=\"' + fmt + '\" ' + (supported ? '' : 'disabled title=\"Not supported by this browser\"') + '>' +\n        '<span class=\"fmt\">' + fmt + '<\/span><span class=\"sz mono\">\u2026<\/span><\/button>';\n    }).join('');\n\n    var qStops = QUALITY_STOPS.map(function(s, i){ return '<span data-i=\"' + i + '\">' + s.label + '<\/span>'; }).join('');\n    var rStops = RES_STOPS.map(function(s, i){ return '<span data-i=\"' + i + '\">' + s.label + '<br>' + s.dpi + '<\/span>'; }).join('');\n\n    el.innerHTML =\n      '<div class=\"pdf-card-head\">' +\n        '<div>' +\n          '<p class=\"pdf-name\">' + escapeHtml(card.name) + '<\/p>' +\n          '<div class=\"pill-row\">' +\n            '<span class=\"pill\">PDF<\/span>' +\n            '<span class=\"meta-text mono\">' + card.numPages + (card.numPages === 1 ? ' page' : ' pages') + '<\/span>' +\n            '<span class=\"meta-text mono\">' + formatBytes(card.size) + '<\/span>' +\n          '<\/div>' +\n        '<\/div>' +\n        '<button class=\"remove\" aria-label=\"Remove\" data-action=\"remove\" style=\"position:static;\">\u00d7<\/button>' +\n      '<\/div>' +\n\n      '<div class=\"pdf-controls\">' +\n        '<div>' +\n          '<div class=\"ctrl-block\"><div class=\"ctrl-label\"><span>Save as<\/span><\/div>' +\n            '<div class=\"chip-row\" data-role=\"fmt-chips\">' + chips + '<\/div>' +\n          '<\/div>' +\n          '<div class=\"ctrl-block\" data-role=\"quality-block\">' +\n            '<div class=\"ctrl-label\"><span>Compression<\/span><span class=\"val mono\" data-role=\"qlabel\"><\/span><\/div>' +\n            '<input type=\"range\" min=\"0\" max=\"5\" step=\"1\" value=\"' + card.settings.qIndex + '\" data-role=\"quality\" \/>' +\n            '<div class=\"stops-row\">' + qStops + '<\/div>' +\n          '<\/div>' +\n        '<\/div>' +\n        '<div>' +\n          '<div class=\"ctrl-block\">' +\n            '<div class=\"ctrl-label\"><span>Resolution<\/span><span class=\"val mono\" data-role=\"rlabel\"><\/span><\/div>' +\n            '<input type=\"range\" min=\"0\" max=\"3\" step=\"1\" value=\"' + card.settings.resIndex + '\" data-role=\"resolution\" \/>' +\n            '<div class=\"stops-row\">' + rStops + '<\/div>' +\n          '<\/div>' +\n          '<div class=\"ctrl-block range-field\">' +\n            '<div class=\"ctrl-label\"><span>Pages<\/span><\/div>' +\n            '<input type=\"text\" data-role=\"range\" placeholder=\"All pages \u2014 e.g. 1-3, 5, 8-10\" \/>' +\n          '<\/div>' +\n        '<\/div>' +\n      '<\/div>' +\n\n      '<div class=\"extract-row\">' +\n        '<button class=\"btn primary\" data-role=\"extract\">Extract pages<\/button>' +\n        '<button class=\"btn\" data-role=\"dlzip\" style=\"display:none;\">Download all (.zip)<\/button>' +\n      '<\/div>' +\n      '<p class=\"progress-line\" data-role=\"progress\"><\/p>' +\n      '<div class=\"page-grid\" data-role=\"page-grid\"><\/div>';\n\n    el.querySelector('[data-action=\"remove\"]').addEventListener('click', function(){ removePdfCard(card.id); });\n\n    function updateQLabel(){\n      var s = QUALITY_STOPS[card.settings.qIndex];\n      el.querySelector('[data-role=\"qlabel\"]').textContent = s.label + ' \u00b7 ' + s.q + '% \u00b7 ' + s.desc;\n    }\n    function updateRLabel(){\n      var s = RES_STOPS[card.settings.resIndex];\n      el.querySelector('[data-role=\"rlabel\"]').textContent = s.label + ' \u00b7 ' + s.dpi;\n    }\n    updateQLabel(); updateRLabel();\n    el.querySelector('[data-role=\"quality-block\"]').style.opacity = card.settings.format === 'png' ? 0.4 : 1;\n    el.querySelector('[data-role=\"quality\"]').disabled = card.settings.format === 'png';\n\n    el.querySelectorAll('[data-role=\"fmt-chips\"] .chip').forEach(function(chip){\n      chip.addEventListener('click', function(){\n        if (chip.disabled) return;\n        el.querySelectorAll('[data-role=\"fmt-chips\"] .chip').forEach(function(c){ c.classList.remove('active'); });\n        chip.classList.add('active');\n        card.settings.format = chip.dataset.fmt;\n        el.querySelector('[data-role=\"quality-block\"]').style.opacity = card.settings.format === 'png' ? 0.4 : 1;\n        el.querySelector('[data-role=\"quality\"]').disabled = card.settings.format === 'png';\n        schedulePreview(card, 0);\n      });\n    });\n\n    el.querySelector('[data-role=\"quality\"]').addEventListener('input', function(e){\n      card.settings.qIndex = parseInt(e.target.value, 10);\n      updateQLabel();\n      schedulePreview(card, 300);\n    });\n    el.querySelector('[data-role=\"resolution\"]').addEventListener('input', function(e){\n      card.settings.resIndex = parseInt(e.target.value, 10);\n      updateRLabel();\n      schedulePreview(card, 300);\n    });\n    el.querySelector('[data-role=\"range\"]').addEventListener('input', function(e){\n      card.settings.range = e.target.value;\n    });\n\n    el.querySelector('[data-role=\"extract\"]').addEventListener('click', function(){ extractPages(card); });\n    el.querySelector('[data-role=\"dlzip\"]').addEventListener('click', function(){ downloadCardZip(card); });\n  }\n\n  function schedulePreview(card, delay){\n    clearTimeout(card.previewTimer);\n    card.previewTimer = setTimeout(function(){ computePagePreviewSizes(card); }, delay);\n  }\n\n  function computePagePreviewSizes(card){\n    if (!card.pdfDoc) return;\n    var el = card.el;\n    var scale = RES_STOPS[card.settings.resIndex].scale;\n    var q = QUALITY_STOPS[card.settings.qIndex].q;\n    var fmts = ['jpeg','png','webp'].filter(function(f){ return SUPPORTED[f]; });\n    Promise.all(fmts.map(function(fmt){\n      return renderPdfPageToBlob(card.pdfDoc, 1, scale, fmt, q)\n        .then(function(blob){ return { fmt: fmt, blob: blob }; })\n        .catch(function(){ return { fmt: fmt, blob: null }; });\n    })).then(function(results){\n      var smallestFmt = null, smallestSize = Infinity;\n      results.forEach(function(r){ if (r.blob && r.blob.size < smallestSize){ smallestSize = r.blob.size; smallestFmt = r.fmt; } });\n      results.forEach(function(r){\n        var chip = el.querySelector('[data-role=\"fmt-chips\"] .chip[data-fmt=\"' + r.fmt + '\"]');\n        if (!chip) return;\n        var sz = chip.querySelector('.sz');\n        if (sz) sz.textContent = r.blob ? ('~' + formatBytes(r.blob.size) + '\/pg') : 'failed';\n        var oldBadge = chip.querySelector('.badge');\n        if (oldBadge) oldBadge.remove();\n        if (r.fmt === smallestFmt){\n          var badge = document.createElement('span');\n          badge.className = 'badge'; badge.textContent = 'smallest';\n          chip.appendChild(badge);\n        }\n      });\n    });\n  }\n\n  function extractPages(card){\n    if (card.extracting || !card.pdfDoc) return;\n    var el = card.el;\n    var pages = parsePageRange(card.settings.range, card.numPages);\n    var progressEl = el.querySelector('[data-role=\"progress\"]');\n    var gridEl = el.querySelector('[data-role=\"page-grid\"]');\n    var extractBtn = el.querySelector('[data-role=\"extract\"]');\n    var zipBtn = el.querySelector('[data-role=\"dlzip\"]');\n    if (pages.length === 0){\n      progressEl.textContent = 'No valid pages in that range.';\n      return;\n    }\n    card.extracting = true;\n    extractBtn.disabled = true;\n    zipBtn.style.display = 'none';\n    gridEl.innerHTML = '';\n    card.pageBlobs = {};\n\n    var scale = RES_STOPS[card.settings.resIndex].scale;\n    var fmt = card.settings.format;\n    var q = QUALITY_STOPS[card.settings.qIndex].q;\n    var digits = String(card.numPages).length;\n\n    var i = 0;\n    function next(){\n      if (i >= pages.length){\n        progressEl.textContent = 'Done \u2014 ' + pages.length + (pages.length === 1 ? ' page' : ' pages') + ' extracted.';\n        card.extracting = false;\n        extractBtn.disabled = false;\n        zipBtn.style.display = '';\n        return;\n      }\n      var pageNum = pages[i];\n      progressEl.textContent = 'Rendering page ' + pageNum + '\u2026 (' + (i + 1) + ' of ' + pages.length + ')';\n      renderPdfPageToBlob(card.pdfDoc, pageNum, scale, fmt, q).then(function(blob){\n        card.pageBlobs[pageNum] = blob;\n        var url = URL.createObjectURL(blob);\n        var tile = document.createElement('div');\n        tile.className = 'page-tile';\n        tile.innerHTML =\n          '<div class=\"frame-thumb\"><span class=\"frame-num mono\">' + pad2(pageNum) + '<\/span><img decoding=\"async\" src=\"' + url + '\" alt=\"\" \/><\/div>' +\n          '<div class=\"frame-meta\">' + formatBytes(blob.size) + '<\/div>' +\n          '<button class=\"page-dl mono\">Download<\/button>';\n        tile.querySelector('.page-dl').addEventListener('click', function(){\n          triggerDownload(url, card.base + '-p' + String(pageNum).padStart(digits, '0') + '.' + EXT[fmt]);\n        });\n        gridEl.appendChild(tile);\n        i++;\n        next();\n      }).catch(function(){\n        progressEl.textContent = 'Failed to render page ' + pageNum + '.';\n        i++;\n        next();\n      });\n    }\n    next();\n  }\n\n  function downloadCardZip(card){\n    if (typeof JSZip === 'undefined'){ alert('The zip library failed to load.'); return; }\n    var fmt = card.settings.format;\n    var digits = String(card.numPages).length;\n    var zip = new JSZip();\n    var any = false;\n    Object.keys(card.pageBlobs).forEach(function(pageNum){\n      var blob = card.pageBlobs[pageNum];\n      if (!blob) return;\n      zip.file(card.base + '-p' + String(pageNum).padStart(digits, '0') + '.' + EXT[fmt], blob);\n      any = true;\n    });\n    if (!any) return;\n    zip.generateAsync({ type: 'blob' }).then(function(content){\n      var url = URL.createObjectURL(content);\n      triggerDownload(url, card.base + '-pages.zip');\n      setTimeout(function(){ URL.revokeObjectURL(url); }, 4000);\n    });\n  }\n\n  function removePdfCard(id){\n    var card = pdfCards.get(id);\n    if (!card) return;\n    if (card.el && card.el.parentNode) card.el.parentNode.removeChild(card.el);\n    pdfCards.delete(id);\n  }\n\n  function wirePdf2Img(){\n    var dz = document.getElementById('dzPdf');\n    var input = document.getElementById('pdfInput');\n    dz.addEventListener('click', function(){ input.click(); });\n    dz.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); input.click(); } });\n    input.addEventListener('change', function(){\n      Array.from(input.files).filter(function(f){ return f.type === 'application\/pdf' || \/\\.pdf$\/i.test(f.name); }).forEach(addPdfFile);\n      input.value = '';\n    });\n    ['dragenter','dragover'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.add('active'); }); });\n    ['dragleave','drop'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.remove('active'); }); });\n    dz.addEventListener('drop', function(e){\n      if (e.dataTransfer && e.dataTransfer.files){\n        Array.from(e.dataTransfer.files).filter(function(f){ return f.type === 'application\/pdf' || \/\\.pdf$\/i.test(f.name); }).forEach(addPdfFile);\n      }\n    });\n  }\n\n  \/\/ ===================================================================\n  \/\/ Tabs + init\n  \/\/ ===================================================================\n  function wireTabs(){\n    var tabImg = document.getElementById('tabImg2Pdf');\n    var tabPdf = document.getElementById('tabPdf2Img');\n    var panelImg = document.getElementById('panelImg2Pdf');\n    var panelPdf = document.getElementById('panelPdf2Img');\n    tabImg.addEventListener('click', function(){\n      tabImg.classList.add('active'); tabImg.setAttribute('aria-selected','true');\n      tabPdf.classList.remove('active'); tabPdf.setAttribute('aria-selected','false');\n      panelImg.classList.add('active'); panelPdf.classList.remove('active');\n    });\n    tabPdf.addEventListener('click', function(){\n      tabPdf.classList.add('active'); tabPdf.setAttribute('aria-selected','true');\n      tabImg.classList.remove('active'); tabImg.setAttribute('aria-selected','false');\n      panelPdf.classList.add('active'); panelImg.classList.remove('active');\n    });\n  }\n\n  function init(){\n    if (typeof pdfjsLib !== 'undefined'){\n      pdfjsLib.GlobalWorkerOptions.workerSrc = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/2.16.105\/pdf.worker.min.js';\n    }\n    detectFormatSupport('image\/webp').then(function(ok){ SUPPORTED.webp = ok; });\n    wireTabs();\n    wireImg2Pdf();\n    wirePdf2Img();\n    document.getElementById('buildStatus').textContent = 'No images yet';\n  }\n\n  document.addEventListener('DOMContentLoaded', init);\n})();\n\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Contact \u2014 image \u21c4 PDF converter \u25cf client-side photo lab CONTACT Lay your images down on a contact sheet and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_eb_attr":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[76],"tags":[],"class_list":["post-4600","post","type-post","status-publish","format-standard","hentry","category-tools"],"_links":{"self":[{"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/posts\/4600","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/comments?post=4600"}],"version-history":[{"count":2,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/posts\/4600\/revisions"}],"predecessor-version":[{"id":4602,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/posts\/4600\/revisions\/4602"}],"wp:attachment":[{"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/media?parent=4600"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/categories?post=4600"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rajmedical.co.in\/hi\/wp-json\/wp\/v2\/tags?post=4600"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}