<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Hashnode - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Hashnode - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 27 Aug 2026 01:03:08 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/hashnode/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Signature Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF documents are commonly used for agreements, forms, approvals, invoices, reports, applications, and other documents that may need a signature or additional text before they are shared. A traditiona ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-signature-tool-javascript/</link>
                <guid isPermaLink="false">6a5e89518186f4c5817d466b</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hashnode ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 20:47:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/175ab1f9-2917-4588-9e67-50607f6fa5a1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF documents are commonly used for agreements, forms, approvals, invoices, reports, applications, and other documents that may need a signature or additional text before they are shared.</p>
<p>A traditional workflow often involves printing the document, signing it by hand, scanning it again, and sending the new file. For a simple electronic signature, that process adds unnecessary steps.</p>
<p>In this tutorial, you'll build a browser-based PDF Signature Tool using JavaScript. Users will be able to upload a PDF, preview and navigate its pages, and add content directly to the document.</p>
<p>The application will support two main element types: <strong>Signature</strong> and <strong>Text/Stamp</strong>.</p>
<p>For signatures, users can draw directly in the browser, type their name and choose a signature style, or upload an existing signature image. For text-based elements, they can enter custom text or use preset stamps such as <strong>APPROVED</strong>, <strong>CONFIDENTIAL</strong>, <strong>DRAFT</strong>, and <strong>PAID</strong>.</p>
<p>After creating an element, users can position it on the PDF preview and adjust properties such as scale, rotation, opacity, font size, and color. The element can then be applied to the current page, every page, or a specific set of pages.</p>
<p>Once processing is complete, the application generates a new PDF for review. Users can preview the result, rename the output file, check its page count and file size, and download it directly from the browser.</p>
<p>The project uses PDF.js for document rendering and PDF-lib for modifying and generating the final PDF.</p>
<p>By the end of this tutorial, you'll understand how to build an interactive PDF editing workflow that combines canvas-based input, image embedding, text placement, coordinate conversion, page selection, and client-side file generation.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-this-pdf-signature-tool-can-do">What This PDF Signature Tool Can Do</a></p>
</li>
<li><p><a href="#heading-electronic-signatures-vs-digital-signatures">Electronic Signatures vs Digital Signatures</a></p>
</li>
<li><p><a href="#heading-how-the-browser-based-workflow-works">How the Browser-Based Workflow Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-libraries-are-we-using">What Libraries Are We Using?</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-the-pdf">Uploading and Previewing the PDF</a></p>
</li>
<li><p><a href="#heading-choosing-an-element-to-add">Choosing an Element to Add</a></p>
</li>
<li><p><a href="#heading-creating-a-signature">Creating a Signature</a></p>
</li>
<li><p><a href="#heading-drawing-a-signature">Drawing a Signature</a></p>
</li>
<li><p><a href="#heading-typing-a-signature">Typing a Signature</a></p>
</li>
<li><p><a href="#heading-uploading-a-signature-image">Uploading a Signature Image</a></p>
</li>
<li><p><a href="#heading-adding-text-and-preset-stamps">Adding Text and Preset Stamps</a></p>
</li>
<li><p><a href="#heading-positioning-and-styling-the-element">Positioning and Styling the Element</a></p>
</li>
<li><p><a href="#heading-applying-the-element-to-selected-pages">Applying the Element to Selected Pages</a></p>
</li>
<li><p><a href="#heading-applying-and-finalizing-the-pdf">Applying and Finalizing the PDF</a></p>
</li>
<li><p><a href="#heading-generating-the-signed-pdf">Generating the Signed PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-final-pdf">Previewing the Final PDF</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading-the-final-pdf">Renaming and Downloading the Final PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-signature-tool-works">Demo: How the PDF Signature Tool Works</a></p>
</li>
<li><p><a href="#heading-handling-signature-transparency">Handling Signature Transparency</a></p>
</li>
<li><p><a href="#heading-important-notes-and-common-mistakes">Important Notes and Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-signature-tool-can-do">What This PDF Signature Tool Can Do</h2>
<p>The application provides a single editing workflow for adding signatures, text, and common document stamps to PDF pages.</p>
<p>When <strong>Signature</strong> is selected, users can create the signature in three different ways.</p>
<ol>
<li><p>The <strong>Draw</strong> option provides a canvas where the user can write a signature using a mouse, trackpad, stylus, or touch input.</p>
</li>
<li><p>The <strong>Type</strong> option converts entered text into a signature-style element. Users can type their name, adjust the size, and choose from the available signature styles.</p>
</li>
<li><p>The <strong>Upload</strong> option accepts an existing signature image. This is useful for someone who already has a transparent PNG or another supported image of their handwritten signature.</p>
</li>
</ol>
<p>The second element type is <strong>Text/Stamp</strong>. Users can enter custom text such as:</p>
<pre><code class="language-text">Signed on: 08-09-2025
</code></pre>
<p>They can also quickly choose a predefined stamp:</p>
<pre><code class="language-text">APPROVED
CONFIDENTIAL
DRAFT
PAID
</code></pre>
<p>After an element has been created, the application provides controls for its placement and appearance. Users can move it to the required location and adjust its scale, rotation, opacity, and position.</p>
<p>Text and stamp elements can additionally use configurable font sizes and colors.</p>
<p>The page controls determine where the selected element will be applied. A signature may belong only on the final page of a contract, while a <code>CONFIDENTIAL</code> stamp may need to appear on every page.</p>
<p>The application therefore supports:</p>
<pre><code class="language-text">Current page only
All pages
Specific pages
</code></pre>
<p>The goal is to provide one consistent workflow for several common PDF editing tasks without requiring separate tools for each element type.</p>
<h2 id="heading-electronic-signatures-vs-digital-signatures">Electronic Signatures vs Digital Signatures</h2>
<p>Before building the application, it's important to distinguish between an <strong>electronic signature</strong> and a <strong>digital signature</strong>.</p>
<p>The tool in this tutorial creates an electronic signature workflow.</p>
<p>A drawn signature, typed signature, or uploaded signature image is placed visually onto the PDF page. This is similar to signing a document by hand and inserting a visible representation of that signature into the file.</p>
<p>For example, a user might draw a signature on a canvas:</p>
<pre><code class="language-javascript">const signatureImage =
    signatureCanvas.toDataURL("image/png");
</code></pre>
<p>The generated image can then be embedded into the PDF.</p>
<p>A digital signature is technically different.</p>
<p>Certificate-based digital signatures use cryptographic methods to help verify document integrity and the identity associated with a signing certificate. They may involve digital certificates, private keys, signature validation, and trust chains.</p>
<p>Simply placing a handwritten signature image on a PDF doesn't create that type of cryptographic verification.</p>
<p>This distinction matters because the terms are sometimes used interchangeably in everyday conversation even though the underlying technologies are different.</p>
<p>The project we're building focuses on <strong>visual electronic signatures and document elements</strong>. It doesn't create certificate-based cryptographic digital signatures.</p>
<p>Keeping that distinction clear makes it easier to understand exactly what the application does and what would require a more advanced signing system.</p>
<h2 id="heading-how-the-browser-based-workflow-works">How the Browser-Based Workflow Works</h2>
<p>The process begins when a user selects a PDF file.</p>
<p>PDF.js loads the document and renders the current page into a browser canvas. Previous and next buttons allow the user to navigate through the PDF before choosing where to place an element.</p>
<p>The user then selects one of two element types:</p>
<pre><code class="language-text">Signature
Text/Stamp
</code></pre>
<p>If <strong>Signature</strong> is selected, the application provides three creation methods:</p>
<pre><code class="language-text">Draw
Type
Upload
</code></pre>
<p>The selected signature is converted into an element that can be displayed over the PDF preview.</p>
<p>If <strong>Text/Stamp</strong> is selected, the application instead creates a text element using either custom content or one of the predefined stamp values.</p>
<p>The complete workflow looks like this:</p>
<pre><code class="language-text">Upload PDF
    ↓
Render and Navigate Pages
    ↓
Choose Signature or Text/Stamp
    ↓
Create the Element
    ↓
Position and Style It
    ↓
Choose Target Pages
    ↓
Apply &amp; Finalize
    ↓
Generate the New PDF
    ↓
Preview the Result
    ↓
Rename and Download
</code></pre>
<p>During editing, the element displayed over the PDF preview is only a browser-side representation. Its position must later be translated into coordinates that match the actual PDF page.</p>
<p>For example, the application may store an element like this:</p>
<pre><code class="language-javascript">const element = {
    type: "signature",
    x: 622,
    y: 496,
    scale: 1.14,
    rotation: 0,
    opacity: 1
};
</code></pre>
<p>When the user clicks <strong>Apply &amp; Finalize</strong>, those values are used to calculate the final placement inside the PDF.</p>
<p>This separation between the interactive preview and the final PDF generation is the foundation of the project. It allows users to visually prepare the document first and create the modified PDF only after the placement is ready.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>To keep the project easy to understand, we'll use three main files:</p>
<pre><code class="language-text">pdf-signature-tool/
│
├── index.html
├── style.css
└── script.js
</code></pre>
<p>The HTML file contains the upload interface, PDF preview, editing controls, final preview, and download section.</p>
<p>The CSS file handles the layout and visual states.</p>
<p>The JavaScript file manages PDF loading, page rendering, signature creation, text and stamp elements, positioning, final PDF generation, and downloading.</p>
<p>Start with the basic HTML structure:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;

    &lt;meta charset="UTF-8"&gt;

    &lt;meta
        name="viewport"
        content="width=device-width, initial-scale=1.0"&gt;

    &lt;title&gt;PDF Signature Tool&lt;/title&gt;

    &lt;link
        rel="stylesheet"
        href="style.css"&gt;

&lt;/head&gt;

&lt;body&gt;

    &lt;main class="pdf-signature-tool"&gt;

        &lt;section id="uploadSection"&gt;

            &lt;h1&gt;PDF Signature Tool&lt;/h1&gt;

            &lt;p&gt;
                Upload your PDF to add your
                electronic signature.
            &lt;/p&gt;

            &lt;div id="dropZone"&gt;

                &lt;p&gt;Drag &amp; Drop PDF Here&lt;/p&gt;

                &lt;p&gt;Or click to browse file&lt;/p&gt;

                &lt;button id="selectPdfButton"&gt;
                    Select PDF
                &lt;/button&gt;

                &lt;input
                    type="file"
                    id="pdfInput"
                    accept="application/pdf"
                    hidden&gt;

            &lt;/div&gt;

        &lt;/section&gt;

        &lt;section
            id="editorSection"
            hidden&gt;

            &lt;div class="pdf-preview"&gt;

                &lt;div id="previewContainer"&gt;

                    &lt;canvas id="pdfCanvas"&gt;&lt;/canvas&gt;

                    &lt;div id="elementLayer"&gt;&lt;/div&gt;

                &lt;/div&gt;

                &lt;div class="page-navigation"&gt;

                    &lt;button id="previousPage"&gt;
                        &amp;lt;
                    &lt;/button&gt;

                    &lt;span id="pageInfo"&gt;
                        Page 1 of 1
                    &lt;/span&gt;

                    &lt;button id="nextPage"&gt;
                        &amp;gt;
                    &lt;/button&gt;

                &lt;/div&gt;

            &lt;/div&gt;

            &lt;aside id="editorControls"&gt;

                &lt;!-- Signature and text controls
                     will be added here --&gt;

            &lt;/aside&gt;

        &lt;/section&gt;

        &lt;section
            id="resultSection"
            hidden&gt;

            &lt;!-- Final preview and download
                 controls will be added here --&gt;

        &lt;/section&gt;

    &lt;/main&gt;

    &lt;script src="script.js"&gt;&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The <code>previewContainer</code> is especially important.</p>
<p>It contains two layers:</p>
<pre><code class="language-text">PDF Canvas
    +
Interactive Element Layer
</code></pre>
<p>The PDF page is rendered onto the canvas, while signatures, text, and stamps are displayed in a separate overlay.</p>
<p>This allows users to move and style an element without modifying the original PDF every time they make a small adjustment.</p>
<p>The overlay should match the dimensions and position of the PDF canvas.</p>
<pre><code class="language-css">#previewContainer {
    position: relative;
    display: inline-block;
}

#pdfCanvas {
    display: block;
}

#elementLayer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
</code></pre>
<p>Individual signature and text elements can later enable their own pointer interactions.</p>
<pre><code class="language-css">.pdf-element {
    position: absolute;
    cursor: move;
    pointer-events: auto;
    transform-origin: center;
}
</code></pre>
<p>This layered structure becomes the foundation of the interactive editor.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>This project uses two JavaScript libraries for different parts of the PDF workflow.</p>
<h3 id="heading-pdfjs-for-rendering-and-previewing">PDF.js for Rendering and Previewing</h3>
<p>PDF.js is responsible for reading the uploaded document and rendering its pages inside the browser.</p>
<p>A page can be loaded like this:</p>
<pre><code class="language-javascript">const page =
    await pdfDocument.getPage(
        currentPage
    );
</code></pre>
<p>The page is then rendered to a canvas:</p>
<pre><code class="language-javascript">const viewport =
    page.getViewport({
        scale: 1.5
    });

const context =
    pdfCanvas.getContext("2d");

pdfCanvas.width =
    viewport.width;

pdfCanvas.height =
    viewport.height;

await page.render({

    canvasContext: context,

    viewport

}).promise;
</code></pre>
<p>PDF.js handles the visual preview.</p>
<h3 id="heading-pdf-lib-for-modifying-the-pdf">PDF-lib for Modifying the PDF</h3>
<p>PDF-lib is used later when the user clicks <strong>Apply &amp; Finalize</strong>.</p>
<p>It allows us to load the original PDF bytes and add content to its pages.</p>
<p>For example:</p>
<pre><code class="language-javascript">const pdfDoc =
    await PDFLib.PDFDocument.load(
        originalPdfBytes
    );
</code></pre>
<p>An uploaded PNG signature can then be embedded:</p>
<pre><code class="language-javascript">const signatureImage =
    await pdfDoc.embedPng(
        signatureBytes
    );
</code></pre>
<p>Text can also be drawn directly onto a PDF page:</p>
<pre><code class="language-javascript">page.drawText(
    "APPROVED",
    {
        x: 100,
        y: 100,
        size: 18
    }
);
</code></pre>
<p>The two libraries therefore have separate responsibilities:</p>
<pre><code class="language-text">PDF.js
→ Load and visually render PDF pages

PDF-lib
→ Modify pages and generate the final PDF
</code></pre>
<p>Separating these responsibilities keeps the editor easier to manage.</p>
<p>Include both libraries in the project before <code>script.js</code>.</p>
<pre><code class="language-html">&lt;script
    src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"&gt;
&lt;/script&gt;

&lt;script
    src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;
&lt;/script&gt;

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker as well:</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =
    "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
</code></pre>
<p>For a production project, pin and test the exact library versions you use rather than automatically loading an unspecified latest release.</p>
<h2 id="heading-uploading-and-previewing-the-pdf">Uploading and Previewing the PDF</h2>
<p>The first interactive step is accepting the user's PDF.</p>
<p>Get references to the required elements:</p>
<pre><code class="language-javascript">const pdfInput =
    document.getElementById(
        "pdfInput"
    );

const selectPdfButton =
    document.getElementById(
        "selectPdfButton"
    );

const dropZone =
    document.getElementById(
        "dropZone"
    );

const uploadSection =
    document.getElementById(
        "uploadSection"
    );

const editorSection =
    document.getElementById(
        "editorSection"
    );

const pdfCanvas =
    document.getElementById(
        "pdfCanvas"
    );
</code></pre>
<p>We also need a few variables to store the current document state.</p>
<pre><code class="language-javascript">let pdfDocument = null;

let originalPdfBytes = null;

let currentPage = 1;

let totalPages = 0;
</code></pre>
<p>Clicking the custom button opens the hidden file input.</p>
<pre><code class="language-javascript">selectPdfButton.addEventListener(
    "click",
    () =&gt; {

        pdfInput.click();

    }
);
</code></pre>
<p>When a file is selected, pass it to the PDF loading function.</p>
<pre><code class="language-javascript">pdfInput.addEventListener(
    "change",
    event =&gt; {

        const file =
            event.target.files[0];

        if (file) {

            loadPdf(file);

        }

    }
);
</code></pre>
<p>Before processing the file, validate its type.</p>
<pre><code class="language-javascript">async function loadPdf(file) {

    if (
        file.type !==
        "application/pdf"
    ) {

        alert(
            "Please select a valid PDF file."
        );

        return;

    }

}
</code></pre>
<p>Read the file as an <code>ArrayBuffer</code>.</p>
<pre><code class="language-javascript">const arrayBuffer =
    await file.arrayBuffer();
</code></pre>
<p>Keep a copy of the original bytes because PDF.js and PDF-lib will use the document at different stages.</p>
<pre><code class="language-javascript">originalPdfBytes =
    new Uint8Array(
        arrayBuffer
    );
</code></pre>
<p>Now load the document with PDF.js.</p>
<pre><code class="language-javascript">pdfDocument =
    await pdfjsLib
        .getDocument({
            data:
                originalPdfBytes.slice()
        })
        .promise;
</code></pre>
<p>Store the number of pages.</p>
<pre><code class="language-javascript">totalPages =
    pdfDocument.numPages;

currentPage = 1;
</code></pre>
<p>Switch from the upload interface to the editor.</p>
<pre><code class="language-javascript">uploadSection.hidden = true;

editorSection.hidden = false;
</code></pre>
<p>Finally, render the first page.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>The complete loading function becomes:</p>
<pre><code class="language-javascript">async function loadPdf(file) {

    if (
        file.type !==
        "application/pdf"
    ) {

        alert(
            "Please select a valid PDF file."
        );

        return;

    }

    const arrayBuffer =
        await file.arrayBuffer();

    originalPdfBytes =
        new Uint8Array(
            arrayBuffer
        );

    pdfDocument =
        await pdfjsLib
            .getDocument({
                data:
                    originalPdfBytes.slice()
            })
            .promise;

    totalPages =
        pdfDocument.numPages;

    currentPage = 1;

    uploadSection.hidden = true;

    editorSection.hidden = false;

    await renderPage(currentPage);

}
</code></pre>
<p>For drag-and-drop support, prevent the browser's default behavior.</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "dragover",
    event =&gt; {

        event.preventDefault();

        dropZone.classList.add(
            "drag-active"
        );

    }
);
</code></pre>
<p>Remove the active state when the file leaves the drop area.</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "dragleave",
    () =&gt; {

        dropZone.classList.remove(
            "drag-active"
        );

    }
);
</code></pre>
<p>Handle the dropped file:</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "drop",
    event =&gt; {

        event.preventDefault();

        dropZone.classList.remove(
            "drag-active"
        );

        const file =
            event.dataTransfer.files[0];

        if (file) {

            loadPdf(file);

        }

    }
);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6605c5ed-31d3-4a2a-a80d-95a77040d890.png" alt="PDF Signature Tool upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="639" height="652" loading="lazy">

<h2 id="heading-rendering-the-current-pdf-page">Rendering the Current PDF Page</h2>
<p>The <code>renderPage()</code> function loads one page from the PDF and displays it on the canvas.</p>
<pre><code class="language-javascript">async function renderPage(
    pageNumber
) {

    const page =
        await pdfDocument.getPage(
            pageNumber
        );

    const viewport =
        page.getViewport({
            scale: 1.5
        });

    const context =
        pdfCanvas.getContext("2d");

    pdfCanvas.width =
        viewport.width;

    pdfCanvas.height =
        viewport.height;

    await page.render({

        canvasContext: context,

        viewport

    }).promise;

    updatePageInfo();

}
</code></pre>
<p>Because the interactive element layer sits above the canvas, it must use the same dimensions.</p>
<pre><code class="language-javascript">const elementLayer =
    document.getElementById(
        "elementLayer"
    );

elementLayer.style.width =
    `${viewport.width}px`;

elementLayer.style.height =
    `${viewport.height}px`;
</code></pre>
<p>Add those lines inside <code>renderPage()</code> after setting the canvas dimensions.</p>
<p>The page information can then be updated:</p>
<pre><code class="language-javascript">function updatePageInfo() {

    pageInfo.textContent =
        `Page ${currentPage} of ${totalPages}`;

}
</code></pre>
<p>At this point, the uploaded PDF page is visible, but users still need a way to move through multi-page documents.</p>
<h2 id="heading-adding-pdf-page-navigation">Adding PDF Page Navigation</h2>
<p>Get the navigation controls:</p>
<pre><code class="language-javascript">const previousPage =
    document.getElementById(
        "previousPage"
    );

const nextPage =
    document.getElementById(
        "nextPage"
    );

const pageInfo =
    document.getElementById(
        "pageInfo"
    );
</code></pre>
<p>The previous button decreases the page number.</p>
<pre><code class="language-javascript">previousPage.addEventListener(
    "click",
    async () =&gt; {

        if (currentPage &lt;= 1) {
            return;
        }

        currentPage--;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>The next button moves forward.</p>
<pre><code class="language-javascript">nextPage.addEventListener(
    "click",
    async () =&gt; {

        if (
            currentPage &gt;=
            totalPages
        ) {
            return;
        }

        currentPage++;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>The boundary checks prevent navigation outside the document.</p>
<p>For a 12-page PDF, the interface may display:</p>
<pre><code class="language-text">Page 12 of 12
</code></pre>
<p>The previous button remains available, while the next action can be disabled because the user is already on the final page.</p>
<pre><code class="language-javascript">function updateNavigationState() {

    previousPage.disabled =
        currentPage === 1;

    nextPage.disabled =
        currentPage ===
        totalPages;

}
</code></pre>
<p>Call this function whenever a new page is rendered.</p>
<pre><code class="language-javascript">function updatePageInfo() {

    pageInfo.textContent =
        `Page ${currentPage} of ${totalPages}`;

    updateNavigationState();

}
</code></pre>
<p>The user can now upload a PDF, preview its pages, and navigate to the exact page where a signature, custom text, or document stamp needs to be placed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e1b4c1e5-f088-4789-af4c-7cb9bb08bdd6.png" alt="Uploaded PDF displayed in the PDF Signature Tool with previous and next page navigation controls." style="display:block;margin:0 auto" width="708" height="550" loading="lazy">

<h2 id="heading-choosing-an-element-to-add">Choosing an Element to Add</h2>
<p>Once the PDF is loaded and the correct page is visible, the user can choose what type of element to place on the document.</p>
<p>The editor provides two options:</p>
<pre><code class="language-text">Signature
Text/Stamp
</code></pre>
<p>Create the element selector:</p>
<pre><code class="language-html">&lt;div class="element-selector"&gt;

    &lt;h3&gt;1. Choose Element&lt;/h3&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="elementType"
            value="signature"
            checked&gt;
        Signature
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="elementType"
            value="text"&gt;
        Text/Stamp
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>Get the controls in JavaScript:</p>
<pre><code class="language-javascript">const elementTypeInputs =
    document.querySelectorAll(
        'input[name="elementType"]'
    );

const signatureControls =
    document.getElementById(
        "signatureControls"
    );

const textControls =
    document.getElementById(
        "textControls"
    );
</code></pre>
<p>Listen for changes:</p>
<pre><code class="language-javascript">elementTypeInputs.forEach(
    input =&gt; {

        input.addEventListener(
            "change",
            event =&gt; {

                const type =
                    event.target.value;

                if (
                    type ===
                    "signature"
                ) {

                    signatureControls.hidden =
                        false;

                    textControls.hidden =
                        true;

                } else {

                    signatureControls.hidden =
                        true;

                    textControls.hidden =
                        false;

                }

            }
        );

    }
);
</code></pre>
<p>This keeps the interface focused. Signature-specific controls appear only when the user is creating a signature, while text and stamp controls appear when that element type is selected.</p>
<h2 id="heading-creating-a-signature">Creating a Signature</h2>
<p>The signature workflow supports three methods:</p>
<pre><code class="language-text">Draw
Type
Upload
</code></pre>
<p>Create the method selector:</p>
<pre><code class="language-html">&lt;div id="signatureControls"&gt;

    &lt;h3&gt;2. Create Signature&lt;/h3&gt;

    &lt;div class="signature-tabs"&gt;

        &lt;button
            data-method="draw"
            class="active"&gt;
            Draw
        &lt;/button&gt;

        &lt;button
            data-method="type"&gt;
            Type
        &lt;/button&gt;

        &lt;button
            data-method="upload"&gt;
            Upload
        &lt;/button&gt;

    &lt;/div&gt;

    &lt;div id="drawPanel"&gt;&lt;/div&gt;

    &lt;div
        id="typePanel"
        hidden&gt;
    &lt;/div&gt;

    &lt;div
        id="uploadPanel"
        hidden&gt;
    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Track the currently selected method:</p>
<pre><code class="language-javascript">let signatureMethod =
    "draw";
</code></pre>
<p>Switch between the three panels:</p>
<pre><code class="language-javascript">const signatureTabs =
    document.querySelectorAll(
        ".signature-tabs button"
    );

signatureTabs.forEach(
    button =&gt; {

        button.addEventListener(
            "click",
            () =&gt; {

                signatureMethod =
                    button.dataset.method;

                showSignatureMethod(
                    signatureMethod
                );

            }
        );

    }
);
</code></pre>
<p>The panel switching function can hide the inactive methods:</p>
<pre><code class="language-javascript">function showSignatureMethod(
    method
) {

    drawPanel.hidden =
        method !== "draw";

    typePanel.hidden =
        method !== "type";

    uploadPanel.hidden =
        method !== "upload";

}
</code></pre>
<p>Each method creates the same type of final element (a signature) but the source of that signature is different.</p>
<h2 id="heading-drawing-a-signature">Drawing a Signature</h2>
<p>The <strong>Draw</strong> option allows users to create a handwritten signature directly in the browser.</p>
<p>Add a canvas to the Draw panel:</p>
<pre><code class="language-html">&lt;div id="drawPanel"&gt;

    &lt;canvas
        id="signatureCanvas"
        width="500"
        height="180"&gt;
    &lt;/canvas&gt;

    &lt;button id="clearSignature"&gt;
        Clear
    &lt;/button&gt;

&lt;/div&gt;
</code></pre>
<p>Get the drawing context:</p>
<pre><code class="language-javascript">const signatureCanvas =
    document.getElementById(
        "signatureCanvas"
    );

const signatureContext =
    signatureCanvas.getContext(
        "2d"
    );

let isDrawing = false;
</code></pre>
<p>Begin drawing when the pointer touches the canvas:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointerdown",
    event =&gt; {

        isDrawing = true;

        const rect =
            signatureCanvas
                .getBoundingClientRect();

        signatureContext.beginPath();

        signatureContext.moveTo(

            event.clientX -
                rect.left,

            event.clientY -
                rect.top

        );

    }
);
</code></pre>
<p>Continue the line while the pointer moves:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointermove",
    event =&gt; {

        if (!isDrawing) {
            return;
        }

        const rect =
            signatureCanvas
                .getBoundingClientRect();

        signatureContext.lineTo(

            event.clientX -
                rect.left,

            event.clientY -
                rect.top

        );

        signatureContext.stroke();

    }
);
</code></pre>
<p>Stop drawing when the pointer is released:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointerup",
    () =&gt; {

        isDrawing = false;

    }
);

signatureCanvas.addEventListener(
    "pointerleave",
    () =&gt; {

        isDrawing = false;

    }
);
</code></pre>
<p>Set a few drawing properties:</p>
<pre><code class="language-javascript">signatureContext.lineWidth = 2;

signatureContext.lineCap =
    "round";

signatureContext.lineJoin =
    "round";
</code></pre>
<p>For touch devices, prevent the browser from interpreting drawing gestures as page scrolling:</p>
<pre><code class="language-css">#signatureCanvas {
    touch-action: none;
    cursor: crosshair;
}
</code></pre>
<p>The Clear button resets the drawing canvas:</p>
<pre><code class="language-javascript">clearSignature.addEventListener(
    "click",
    () =&gt; {

        signatureContext.clearRect(

            0,
            0,

            signatureCanvas.width,
            signatureCanvas.height

        );

    }
);
</code></pre>
<p>Once the signature is ready, convert the canvas into a PNG data URL:</p>
<pre><code class="language-javascript">const drawnSignature =
    signatureCanvas.toDataURL(
        "image/png"
    );
</code></pre>
<p>Because the canvas can preserve transparency, the resulting signature can be placed over the PDF without adding an unwanted rectangular background.</p>
<p>The generated image can now be displayed inside the interactive element layer.</p>
<pre><code class="language-javascript">function useDrawnSignature() {

    const image =
        new Image();

    image.src =
        signatureCanvas.toDataURL(
            "image/png"
        );

    image.onload =
        () =&gt; {

            createSignatureElement(
                image.src
            );

        };

}
</code></pre>
<p>This gives the user a visual signature element that can later be positioned over the PDF page.</p>
<h2 id="heading-typing-a-signature">Typing a Signature</h2>
<p>Not every user has a touchscreen, stylus, or existing signature image.</p>
<p>The <strong>Type</strong> option allows users to enter their name and choose a signature-style appearance.</p>
<p>Add the input controls:</p>
<pre><code class="language-html">&lt;div id="typePanel" hidden&gt;

    &lt;input
        type="text"
        id="typedSignature"
        placeholder="Type your name"&gt;

    &lt;label for="signatureFontSize"&gt;
        Font Size
    &lt;/label&gt;

    &lt;input
        type="number"
        id="signatureFontSize"
        value="45"
        min="12"
        max="120"&gt;

    &lt;div id="signatureStyles"&gt;
    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Listen for text changes:</p>
<pre><code class="language-javascript">typedSignature.addEventListener(
    "input",
    updateTypedSignatures
);
</code></pre>
<p>Create several style previews:</p>
<pre><code class="language-javascript">const signatureFonts = [

    "cursive",

    "'Brush Script MT', cursive",

    "'Segoe Script', cursive"

];
</code></pre>
<p>Render the available options:</p>
<pre><code class="language-javascript">function updateTypedSignatures() {

    const value =
        typedSignature.value.trim();

    signatureStyles.innerHTML = "";

    if (!value) {
        return;
    }

    signatureFonts.forEach(
        font =&gt; {

            const option =
                document.createElement(
                    "button"
                );

            option.textContent =
                value;

            option.style.fontFamily =
                font;

            option.style.fontSize =
                `${signatureFontSize.value}px`;

            option.addEventListener(
                "click",
                () =&gt; {

                    createTypedSignature(
                        value,
                        font
                    );

                }
            );

            signatureStyles.appendChild(
                option
            );

        }
    );

}
</code></pre>
<p>A typed signature can be converted to an image using another canvas.</p>
<pre><code class="language-javascript">function createTypedSignature(
    text,
    fontFamily
) {

    const canvas =
        document.createElement(
            "canvas"
        );

    const context =
        canvas.getContext("2d");

    const fontSize =
        Number(
            signatureFontSize.value
        );

    context.font =
        `${fontSize}px ${fontFamily}`;

    const width =
        context.measureText(
            text
        ).width;

    canvas.width =
        Math.ceil(width + 40);

    canvas.height =
        Math.ceil(fontSize * 2);

    context.font =
        `${fontSize}px ${fontFamily}`;

    context.textBaseline =
        "middle";

    context.fillText(
        text,
        20,
        canvas.height / 2
    );

    const imageUrl =
        canvas.toDataURL(
            "image/png"
        );

    createSignatureElement(
        imageUrl
    );

}
</code></pre>
<p>The typed signature is now treated like the drawn signature: it becomes an image element that can be positioned and later embedded into the PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ebb9405-53f4-480d-b71e-cfb746f6fd5a.png" alt="Typed signature option showing a name, font size control, and multiple signature-style previews." style="display:block;margin:0 auto" width="334" height="682" loading="lazy">

<h2 id="heading-uploading-a-signature-image">Uploading a Signature Image</h2>
<p>The third option allows users to upload an existing signature image.</p>
<p>Add a file input:</p>
<pre><code class="language-html">&lt;div id="uploadPanel" hidden&gt;

    &lt;input
        type="file"
        id="signatureUpload"
        accept="image/png,image/jpeg"&gt;

    &lt;p id="selectedSignatureFile"&gt;
        No file chosen
    &lt;/p&gt;

&lt;/div&gt;
</code></pre>
<p>Listen for file selection:</p>
<pre><code class="language-javascript">signatureUpload.addEventListener(
    "change",
    event =&gt; {

        const file =
            event.target.files[0];

        if (!file) {
            return;
        }

        loadSignatureImage(file);

    }
);
</code></pre>
<p>Validate the image:</p>
<pre><code class="language-javascript">function loadSignatureImage(
    file
) {

    const allowedTypes = [

        "image/png",

        "image/jpeg"

    ];

    if (
        !allowedTypes.includes(
            file.type
        )
    ) {

        alert(
            "Please upload a PNG or JPEG image."
        );

        return;

    }

}
</code></pre>
<p>Read the selected image:</p>
<pre><code class="language-javascript">const reader =
    new FileReader();

reader.onload =
    event =&gt; {

        createSignatureElement(
            event.target.result
        );

};

reader.readAsDataURL(file);
</code></pre>
<p>Display the selected filename:</p>
<pre><code class="language-javascript">selectedSignatureFile.textContent =
    `Selected: ${file.name}`;
</code></pre>
<p>The complete function becomes:</p>
<pre><code class="language-javascript">function loadSignatureImage(
    file
) {

    const allowedTypes = [

        "image/png",

        "image/jpeg"

    ];

    if (
        !allowedTypes.includes(
            file.type
        )
    ) {

        alert(
            "Please upload a PNG or JPEG image."
        );

        return;

    }

    selectedSignatureFile.textContent =
        `Selected: ${file.name}`;

    const reader =
        new FileReader();

    reader.onload =
        event =&gt; {

            createSignatureElement(
                event.target.result
            );

        };

    reader.readAsDataURL(file);

}
</code></pre>
<p>A transparent PNG usually works particularly well because only the signature strokes remain visible over the document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/64561974-9b33-4391-a612-9966707433a1.png" alt="Upload signature option with a selected signature image displayed and positioned on the PDF preview." style="display:block;margin:0 auto" width="345" height="532" loading="lazy">

<h2 id="heading-creating-the-signature-preview-element">Creating the Signature Preview Element</h2>
<p>All three signature methods eventually call the same function:</p>
<pre><code class="language-javascript">createSignatureElement(imageUrl);
</code></pre>
<p>This means the rest of the editor doesn't need separate positioning logic for drawn, typed, and uploaded signatures.</p>
<p>Create the preview element:</p>
<pre><code class="language-javascript">let activeElement = null;

function createSignatureElement(
    imageUrl
) {

    elementLayer.innerHTML = "";

    const image =
        document.createElement(
            "img"
        );

    image.src =
        imageUrl;

    image.className =
        "pdf-element signature-element";

    image.style.left =
        "100px";

    image.style.top =
        "100px";

    image.style.width =
        "180px";

    elementLayer.appendChild(
        image
    );

    activeElement = {

        type: "signature",

        source: imageUrl,

        element: image,

        x: 100,

        y: 100,

        scale: 1,

        rotation: 0,

        opacity: 1

    };

}
</code></pre>
<p>The preview now represents the signature that will eventually be written into the PDF.</p>
<p>The same state object can later be updated when the user changes the signature's position, scale, rotation, or opacity.</p>
<h2 id="heading-adding-text-and-preset-stamps">Adding Text and Preset Stamps</h2>
<p>The second main element type is <strong>Text/Stamp</strong>.</p>
<p>This mode is useful when a document needs a short label, status, date, or other text rather than a handwritten signature.</p>
<p>Create the controls:</p>
<pre><code class="language-html">&lt;div id="textControls" hidden&gt;

    &lt;h3&gt;2. Add Text or Stamp&lt;/h3&gt;

    &lt;input
        type="text"
        id="customText"
        placeholder="Enter text"&gt;

    &lt;div class="stamp-options"&gt;

        &lt;button data-stamp="APPROVED"&gt;
            APPROVED
        &lt;/button&gt;

        &lt;button data-stamp="CONFIDENTIAL"&gt;
            CONFIDENTIAL
        &lt;/button&gt;

        &lt;button data-stamp="DRAFT"&gt;
            DRAFT
        &lt;/button&gt;

        &lt;button data-stamp="PAID"&gt;
            PAID
        &lt;/button&gt;

    &lt;/div&gt;

    &lt;label&gt;
        Size

        &lt;input
            type="number"
            id="textSize"
            value="16"
            min="8"
            max="120"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Color

        &lt;input
            type="color"
            id="textColor"
            value="#000000"&gt;
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>Custom text can be displayed as the user types:</p>
<pre><code class="language-javascript">customText.addEventListener(
    "input",
    () =&gt; {

        createTextElement(
            customText.value
        );

    }
);
</code></pre>
<p>Preset stamps can update the same text input:</p>
<pre><code class="language-javascript">const stampButtons =
    document.querySelectorAll(
        "[data-stamp]"
    );

stampButtons.forEach(
    button =&gt; {

        button.addEventListener(
            "click",
            () =&gt; {

                const stamp =
                    button.dataset.stamp;

                customText.value =
                    stamp;

                createTextElement(
                    stamp
                );

            }
        );

    }
);
</code></pre>
<p>Create the text preview:</p>
<pre><code class="language-javascript">function createTextElement(
    text
) {

    if (!text.trim()) {

        elementLayer.innerHTML = "";

        activeElement = null;

        return;

    }

    elementLayer.innerHTML = "";

    const textElement =
        document.createElement(
            "div"
        );

    textElement.className =
        "pdf-element text-element";

    textElement.textContent =
        text;

    textElement.style.left =
        "100px";

    textElement.style.top =
        "100px";

    textElement.style.fontSize =
        `${textSize.value}px`;

    textElement.style.color =
        textColor.value;

    elementLayer.appendChild(
        textElement
    );

    activeElement = {

        type: "text",

        text,

        element:
            textElement,

        x: 100,

        y: 100,

        fontSize:
            Number(
                textSize.value
            ),

        color:
            textColor.value,

        rotation: 0,

        opacity: 1

    };

}
</code></pre>
<p>When the size changes, update the current element:</p>
<pre><code class="language-javascript">textSize.addEventListener(
    "input",
    () =&gt; {

        if (
            activeElement?.type !==
            "text"
        ) {
            return;
        }

        activeElement.fontSize =
            Number(
                textSize.value
            );

        activeElement
            .element
            .style
            .fontSize =
                `${textSize.value}px`;

    }
);
</code></pre>
<p>Do the same for the color:</p>
<pre><code class="language-javascript">textColor.addEventListener(
    "input",
    () =&gt; {

        if (
            activeElement?.type !==
            "text"
        ) {
            return;
        }

        activeElement.color =
            textColor.value;

        activeElement
            .element
            .style
            .color =
                textColor.value;

    }
);
</code></pre>
<p>The user can now enter custom content such as:</p>
<pre><code class="language-text">Signed on: 08-09-2025
</code></pre>
<p>or quickly select a predefined document stamp.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/18e1bb24-c802-4a40-b7dc-87772fbdbf0c.png" alt="Text and stamp editor showing custom text, Approved, Confidential, Draft, and Paid preset options with font size and color controls." style="display:block;margin:0 auto" width="1062" height="568" loading="lazy">

<p>At this point, the application can create content using all of the available input methods: a drawn signature, typed signature, uploaded signature image, custom text, or preset document stamp.</p>
<h2 id="heading-positioning-and-styling-the-element">Positioning and Styling the Element</h2>
<p>After creating a signature, text label, or preset stamp, the next step is positioning it correctly on the PDF page.</p>
<p>The preview element sits inside the <code>elementLayer</code> created earlier. Because this layer matches the PDF canvas dimensions, users can move the element visually before anything is written into the final PDF.</p>
<p>The editor also provides controls for:</p>
<ul>
<li><p>Scale</p>
</li>
<li><p>Rotation</p>
</li>
<li><p>Opacity</p>
</li>
<li><p>X position</p>
</li>
<li><p>Y position</p>
</li>
</ul>
<p>The exact controls can vary depending on the active element. For example, scale is particularly useful for signatures, while text size and color are handled by the Text/Stamp controls from the previous section.</p>
<p>Create the placement controls:</p>
<pre><code class="language-html">&lt;div id="placementControls"&gt;

    &lt;h3&gt;3. Placement &amp; Style&lt;/h3&gt;

    &lt;label&gt;
        Rotation (°)

        &lt;input
            type="number"
            id="rotationInput"
            value="0"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Opacity

        &lt;input
            type="range"
            id="opacityInput"
            min="0"
            max="100"
            value="100"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        X Position

        &lt;input
            type="number"
            id="xPosition"
            value="100"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Y Position

        &lt;input
            type="number"
            id="yPosition"
            value="100"&gt;
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>For signature elements, add a scale control:</p>
<pre><code class="language-html">&lt;label&gt;
    Scale (&lt;span id="scaleValue"&gt;100%&lt;/span&gt;)

    &lt;input
        type="range"
        id="scaleInput"
        min="25"
        max="250"
        value="100"&gt;
&lt;/label&gt;
</code></pre>
<p>Get the controls in JavaScript:</p>
<pre><code class="language-javascript">const scaleInput =
    document.getElementById(
        "scaleInput"
    );

const scaleValue =
    document.getElementById(
        "scaleValue"
    );

const rotationInput =
    document.getElementById(
        "rotationInput"
    );

const opacityInput =
    document.getElementById(
        "opacityInput"
    );

const xPosition =
    document.getElementById(
        "xPosition"
    );

const yPosition =
    document.getElementById(
        "yPosition"
    );
</code></pre>
<p>We'll use a single function to update the visual transformation.</p>
<pre><code class="language-javascript">function updateElementTransform() {

    if (!activeElement) {
        return;
    }

    activeElement.element.style.transform =
        `
            scale(${activeElement.scale})
            rotate(${activeElement.rotation}deg)
        `;

    activeElement.element.style.opacity =
        activeElement.opacity;

}
</code></pre>
<p>For text elements, initialize <code>scale</code> as <code>1</code> so the same transformation function can still be used.</p>
<pre><code class="language-javascript">activeElement = {

    type: "text",

    text,

    element: textElement,

    x: 100,

    y: 100,

    scale: 1,

    rotation: 0,

    opacity: 1

};
</code></pre>
<h3 id="heading-changing-the-element-scale">Changing the Element Scale</h3>
<p>When the user moves the scale slider, convert the percentage into a decimal value.</p>
<pre><code class="language-javascript">scaleInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const percentage =
            Number(
                scaleInput.value
            );

        activeElement.scale =
            percentage / 100;

        scaleValue.textContent =
            `${percentage}%`;

        updateElementTransform();

    }
);
</code></pre>
<p>A value of <code>100%</code> represents the original preview size.</p>
<pre><code class="language-text">50%  → 0.5
100% → 1
114% → 1.14
200% → 2
</code></pre>
<p>This makes it easy to enlarge or reduce an uploaded, drawn, or typed signature without creating a new image.</p>
<h3 id="heading-rotating-the-element">Rotating the Element</h3>
<p>The rotation input stores the angle in degrees.</p>
<pre><code class="language-javascript">rotationInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        activeElement.rotation =
            Number(
                rotationInput.value
            );

        updateElementTransform();

    }
);
</code></pre>
<p>A rotation of <code>0</code> keeps the element horizontal, while positive or negative values rotate it around its center.</p>
<h3 id="heading-adjusting-opacity">Adjusting Opacity</h3>
<p>Opacity can be useful for stamps, watermarks, and other document labels.</p>
<p>Convert the percentage slider to a value between <code>0</code> and <code>1</code>.</p>
<pre><code class="language-javascript">opacityInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        activeElement.opacity =
            Number(
                opacityInput.value
            ) / 100;

        updateElementTransform();

    }
);
</code></pre>
<p>For example:</p>
<pre><code class="language-text">100% → 1
75%  → 0.75
50%  → 0.5
</code></pre>
<p>The same opacity value will later be used when generating the final PDF.</p>
<h2 id="heading-dragging-an-element-across-the-pdf-preview">Dragging an Element Across the PDF Preview</h2>
<p>Typing X and Y coordinates manually is useful for precise adjustments, but most users will prefer to drag the element directly to the required location.</p>
<p>Track the dragging state:</p>
<pre><code class="language-javascript">let isDragging = false;

let dragOffsetX = 0;

let dragOffsetY = 0;
</code></pre>
<p>When a signature or text element is created, attach the dragging behavior.</p>
<pre><code class="language-javascript">function enableDragging(
    element
) {

    element.addEventListener(
        "pointerdown",
        event =&gt; {

            isDragging = true;

            const elementRect =
                element
                    .getBoundingClientRect();

            dragOffsetX =
                event.clientX -
                elementRect.left;

            dragOffsetY =
                event.clientY -
                elementRect.top;

            element.setPointerCapture(
                event.pointerId
            );

        }
    );

}
</code></pre>
<p>Call this function when creating an element.</p>
<pre><code class="language-javascript">enableDragging(image);
</code></pre>
<p>or:</p>
<pre><code class="language-javascript">enableDragging(textElement);
</code></pre>
<p>Next, listen for pointer movement.</p>
<pre><code class="language-javascript">elementLayer.addEventListener(
    "pointermove",
    event =&gt; {

        if (
            !isDragging ||
            !activeElement
        ) {
            return;
        }

        const layerRect =
            elementLayer
                .getBoundingClientRect();

        const x =
            event.clientX -
            layerRect.left -
            dragOffsetX;

        const y =
            event.clientY -
            layerRect.top -
            dragOffsetY;

        moveActiveElement(
            x,
            y
        );

    }
);
</code></pre>
<p>Create a reusable movement function:</p>
<pre><code class="language-javascript">function moveActiveElement(
    x,
    y
) {

    if (!activeElement) {
        return;
    }

    activeElement.x = x;

    activeElement.y = y;

    activeElement.element.style.left =
        `${x}px`;

    activeElement.element.style.top =
        `${y}px`;

    xPosition.value =
        Math.round(x);

    yPosition.value =
        Math.round(y);

}
</code></pre>
<p>Stop dragging when the pointer is released.</p>
<pre><code class="language-javascript">elementLayer.addEventListener(
    "pointerup",
    () =&gt; {

        isDragging = false;

    }
);

elementLayer.addEventListener(
    "pointercancel",
    () =&gt; {

        isDragging = false;

    }
);
</code></pre>
<p>Now the signature or text element can be moved directly over the document.</p>
<p>For example, an uploaded signature may be positioned near the bottom-right corner of the final page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/77fa703f-4636-4423-bb05-b97fc163d6de.png" alt="Uploaded signature image positioned on a PDF page with scale, rotation, opacity, X position, and Y position controls." style="display:block;margin:0 auto" width="1047" height="532" loading="lazy">

<h2 id="heading-updating-the-position-manually">Updating the Position Manually</h2>
<p>The X and Y fields provide another way to position the element.</p>
<p>Listen for changes to the X coordinate:</p>
<pre><code class="language-javascript">xPosition.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const x =
            Number(
                xPosition.value
            );

        moveActiveElement(
            x,
            activeElement.y
        );

    }
);
</code></pre>
<p>Do the same for Y:</p>
<pre><code class="language-javascript">yPosition.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const y =
            Number(
                yPosition.value
            );

        moveActiveElement(
            activeElement.x,
            y
        );

    }
);
</code></pre>
<p>Dragging and manual coordinate entry remain synchronized. Moving the element updates the fields, while changing the fields moves the preview element.</p>
<h2 id="heading-keeping-the-element-inside-the-page">Keeping the Element Inside the Page</h2>
<p>Without boundaries, users could accidentally drag an element completely outside the PDF preview.</p>
<p>We can limit the position before saving it.</p>
<pre><code class="language-javascript">function clampPosition(
    x,
    y
) {

    const element =
        activeElement.element;

    const maxX =
        elementLayer.clientWidth -
        element.offsetWidth;

    const maxY =
        elementLayer.clientHeight -
        element.offsetHeight;

    return {

        x:
            Math.max(
                0,
                Math.min(x, maxX)
            ),

        y:
            Math.max(
                0,
                Math.min(y, maxY)
            )

    };

}
</code></pre>
<p>Use it inside <code>moveActiveElement()</code>:</p>
<pre><code class="language-javascript">const position =
    clampPosition(
        x,
        y
    );

activeElement.x =
    position.x;

activeElement.y =
    position.y;
</code></pre>
<p>When scale or rotation is applied, the element's transformed visual bounds can extend beyond its original box. A production editor can use <code>getBoundingClientRect()</code> for more precise transformed-boundary calculations.</p>
<p>The basic clamp shown here is sufficient to demonstrate the positioning workflow.</p>
<h2 id="heading-applying-the-element-to-selected-pages">Applying the Element to Selected Pages</h2>
<p>After positioning the element, the user decides which PDF pages should receive it.</p>
<p>The interface provides three options:</p>
<pre><code class="language-text">Current page only
All pages
Specific pages
</code></pre>
<p>Create the controls:</p>
<pre><code class="language-html">&lt;div id="pageApplication"&gt;

    &lt;h3&gt;4. Apply to Pages&lt;/h3&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="current"
            checked&gt;
        Current page only
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="all"&gt;
        All pages
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="specific"&gt;
        Specific pages
    &lt;/label&gt;

    &lt;input
        type="text"
        id="specificPages"
        placeholder="e.g., 1, 3-5, 10"&gt;

&lt;/div&gt;
</code></pre>
<p>Read the selected mode:</p>
<pre><code class="language-javascript">function getTargetPages() {

    const mode =
        document.querySelector(
            'input[name="applyMode"]:checked'
        ).value;

    if (
        mode ===
        "current"
    ) {

        return [
            currentPage
        ];

    }

    if (
        mode ===
        "all"
    ) {

        return Array.from(

            {
                length:
                    totalPages
            },

            (_, index) =&gt;
                index + 1

        );

    }

    return parsePageRange(
        specificPages.value
    );

}
</code></pre>
<p>Parse custom values such as:</p>
<pre><code class="language-text">1, 3-5, 10
</code></pre>
<p>with:</p>
<pre><code class="language-javascript">function parsePageRange(
    value
) {

    const pages =
        new Set();

    value
        .split(",")
        .forEach(part =&gt; {

            const item =
                part.trim();

            if (!item) {
                return;
            }

            if (
                item.includes("-")
            ) {

                const [
                    start,
                    end
                ] =
                    item
                        .split("-")
                        .map(Number);

                for (
                    let page = start;
                    page &lt;= end;
                    page++
                ) {

                    if (
                        page &gt;= 1 &amp;&amp;
                        page &lt;= totalPages
                    ) {

                        pages.add(page);

                    }

                }

            } else {

                const page =
                    Number(item);

                if (
                    page &gt;= 1 &amp;&amp;
                    page &lt;= totalPages
                ) {

                    pages.add(page);

                }

            }

        });

    return [...pages];

}
</code></pre>
<p>The result becomes:</p>
<pre><code class="language-javascript">[
    1,
    3,
    4,
    5,
    10
]
</code></pre>
<p>This allows a signature or stamp to be placed once and then applied to multiple target pages.</p>
<p>Just keep in mind that page dimensions may differ within the same PDF. Applying the same coordinates across pages works best when those pages use a consistent size and layout.</p>
<h2 id="heading-applying-and-finalizing-the-pdf">Applying and Finalizing the PDF</h2>
<p>Once the element is created, positioned, styled, and assigned to the correct pages, the user can click <strong>Apply &amp; Finalize</strong>.</p>
<p>Create the action buttons:</p>
<pre><code class="language-html">&lt;div class="editor-actions"&gt;

    &lt;button id="applyButton"&gt;
        Apply &amp; Finalize
    &lt;/button&gt;

    &lt;button id="startOverButton"&gt;
        Start Over
    &lt;/button&gt;

&lt;/div&gt;
</code></pre>
<p>Get the buttons:</p>
<pre><code class="language-javascript">const applyButton =
    document.getElementById(
        "applyButton"
    );

const startOverButton =
    document.getElementById(
        "startOverButton"
    );
</code></pre>
<p>Before generating the final PDF, make sure an element exists.</p>
<pre><code class="language-javascript">applyButton.addEventListener(
    "click",
    async () =&gt; {

        if (!activeElement) {

            alert(
                "Please add a signature, text, or stamp first."
            );

            return;

        }

        const targetPages =
            getTargetPages();

        if (
            targetPages.length === 0
        ) {

            alert(
                "Please select at least one valid page."
            );

            return;

        }

        await generateFinalPdf(
            targetPages
        );

    }
);
</code></pre>
<p>The <code>generateFinalPdf()</code> function will handle the actual PDF modification in the next section.</p>
<p>The <strong>Start Over</strong> button clears the current document and resets the application.</p>
<pre><code class="language-javascript">startOverButton.addEventListener(
    "click",
    resetTool
);
</code></pre>
<p>Create the reset function:</p>
<pre><code class="language-javascript">function resetTool() {

    pdfDocument = null;

    originalPdfBytes = null;

    currentPage = 1;

    totalPages = 0;

    activeElement = null;

    pdfInput.value = "";

    elementLayer.innerHTML = "";

    editorSection.hidden = true;

    resultSection.hidden = true;

    uploadSection.hidden = false;

}
</code></pre>
<p>This returns the application to its original upload state.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e98a951a-609b-4e1e-b2a6-5cd2e31a6ac1.png" alt="Apply and Finalize button for adding the selected signature or text element to the PDF, with a Start Over option." style="display:block;margin:0 auto" width="362" height="86" loading="lazy">

<p>The interactive editing stage is now complete. Users can create a signature or text element, position it directly over the PDF, adjust its appearance, choose the target pages, and prepare the document for final processing.</p>
<h2 id="heading-generating-the-signed-pdf">Generating the Signed PDF</h2>
<p>The element shown over the browser preview hasn't yet been added to the actual PDF. When users click <strong>Apply &amp; Finalize</strong>, the application loads the original document with PDF-lib and writes the selected signature, text, or stamp onto the target pages.</p>
<p>Start by loading the original PDF bytes:</p>
<pre><code class="language-javascript">async function generateFinalPdf(
    targetPages
) {

    const pdfDoc =
        await PDFLib.PDFDocument.load(
            originalPdfBytes.slice()
        );

    const pages =
        pdfDoc.getPages();

}
</code></pre>
<p>Before placing the element, we need to convert its browser coordinates into PDF coordinates.</p>
<p>The preview canvas may be displayed at a different size from the actual PDF page. The coordinate systems also use different Y-axis origins.</p>
<p>For each target page, calculate the scale:</p>
<pre><code class="language-javascript">const {
    width: pdfWidth,
    height: pdfHeight
} = page.getSize();

const scaleX =
    pdfWidth /
    pdfCanvas.width;

const scaleY =
    pdfHeight /
    pdfCanvas.height;
</code></pre>
<p>Convert the preview position:</p>
<pre><code class="language-javascript">const pdfX =
    activeElement.x *
    scaleX;

const pdfY =
    pdfHeight -
    (
        activeElement.y +
        activeElement.element.offsetHeight
    ) * scaleY;
</code></pre>
<p>This conversion maps the element from the browser's top-left coordinate system to the PDF page's coordinate system.</p>
<h3 id="heading-embedding-a-signature">Embedding a Signature</h3>
<p>Drawn, typed, and uploaded signatures are all represented as images by the time they reach the final processing stage.</p>
<p>Convert the signature data URL into bytes:</p>
<pre><code class="language-javascript">async function dataUrlToBytes(
    dataUrl
) {

    const response =
        await fetch(dataUrl);

    return await response.arrayBuffer();

}
</code></pre>
<p>Embed the signature image:</p>
<pre><code class="language-javascript">const signatureBytes =
    await dataUrlToBytes(
        activeElement.source
    );

const signatureImage =
    await pdfDoc.embedPng(
        signatureBytes
    );
</code></pre>
<p>If uploaded JPEG signatures are supported, the application should preserve the original image format and use <code>embedJpg()</code> when appropriate.</p>
<p>Calculate the final dimensions:</p>
<pre><code class="language-javascript">const previewWidth =
    activeElement
        .element
        .offsetWidth *
    activeElement.scale;

const previewHeight =
    activeElement
        .element
        .offsetHeight *
    activeElement.scale;

const finalWidth =
    previewWidth *
    scaleX;

const finalHeight =
    previewHeight *
    scaleY;
</code></pre>
<p>Then draw the signature:</p>
<pre><code class="language-javascript">page.drawImage(
    signatureImage,
    {
        x: pdfX,

        y:
            pdfHeight -
            (
                activeElement.y *
                scaleY
            ) -
            finalHeight,

        width:
            finalWidth,

        height:
            finalHeight,

        rotate:
            PDFLib.degrees(
                activeElement.rotation
            ),

        opacity:
            activeElement.opacity
    }
);
</code></pre>
<p>The same processing logic works whether the signature was drawn, typed, or uploaded because all three methods produce an image element before finalization.</p>
<h3 id="heading-adding-text-or-a-stamp">Adding Text or a Stamp</h3>
<p>Text and preset stamps are written directly onto the PDF page.</p>
<p>First, convert the selected color from hexadecimal to RGB values.</p>
<pre><code class="language-javascript">function hexToRgb(
    hex
) {

    const value =
        hex.replace(
            "#",
            ""
        );

    return {

        r:
            parseInt(
                value.substring(0, 2),
                16
            ) / 255,

        g:
            parseInt(
                value.substring(2, 4),
                16
            ) / 255,

        b:
            parseInt(
                value.substring(4, 6),
                16
            ) / 255

    };

}
</code></pre>
<p>Apply the text:</p>
<pre><code class="language-javascript">const color =
    hexToRgb(
        activeElement.color
    );

page.drawText(
    activeElement.text,
    {
        x:
            activeElement.x *
            scaleX,

        y:
            pdfHeight -
            (
                activeElement.y *
                scaleY
            ) -
            activeElement.fontSize,

        size:
            activeElement.fontSize *
            scaleY,

        color:
            PDFLib.rgb(
                color.r,
                color.g,
                color.b
            ),

        rotate:
            PDFLib.degrees(
                activeElement.rotation
            ),

        opacity:
            activeElement.opacity
    }
);
</code></pre>
<p>After processing every target page, save the modified document:</p>
<pre><code class="language-javascript">const finalPdfBytes =
    await pdfDoc.save();

const finalPdfBlob =
    new Blob(
        [finalPdfBytes],
        {
            type:
                "application/pdf"
        }
    );

await showFinalPreview(
    finalPdfBlob
);
</code></pre>
<p>At this point, the selected signature, text, or stamp has been added to the generated PDF.</p>
<h2 id="heading-previewing-the-final-pdf">Previewing the Final PDF</h2>
<p>Before downloading the document, the application displays the completed PDF in a separate preview area.</p>
<p>This allows users to confirm that the element appears on the correct page and in the expected position.</p>
<p>Load the generated file with PDF.js:</p>
<pre><code class="language-javascript">let finalPdfDocument = null;

let finalPage = 1;

async function showFinalPreview(
    blob
) {

    const bytes =
        await blob.arrayBuffer();

    finalPdfDocument =
        await pdfjsLib
            .getDocument({
                data: bytes
            })
            .promise;

    finalPage = 1;

    editorSection.hidden =
        true;

    resultSection.hidden =
        false;

    await renderFinalPage(
        finalPage
    );

}
</code></pre>
<p>Render the current result page:</p>
<pre><code class="language-javascript">async function renderFinalPage(
    pageNumber
) {

    const page =
        await finalPdfDocument
            .getPage(
                pageNumber
            );

    const viewport =
        page.getViewport({
            scale: 1.4
        });

    finalCanvas.width =
        viewport.width;

    finalCanvas.height =
        viewport.height;

    await page.render({

        canvasContext:
            finalCanvas
                .getContext("2d"),

        viewport

    }).promise;

    finalPageInfo.textContent =
        `Page ${pageNumber} of ${finalPdfDocument.numPages}`;

}
</code></pre>
<p>Previous and next controls can use the same navigation pattern as the original PDF preview.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ca71960-392a-4c49-a138-f462786682a0.png" alt="Final PDF preview showing the applied signature or text element before downloading the document." style="display:block;margin:0 auto" width="704" height="547" loading="lazy">

<h2 id="heading-renaming-and-downloading-the-final-pdf">Renaming and Downloading the Final PDF</h2>
<p>After reviewing the processed document, users can rename the file before downloading it.</p>
<p>For example:</p>
<pre><code class="language-text">document_signed.pdf
</code></pre>
<p>Create the filename input:</p>
<pre><code class="language-html">&lt;input
    type="text"
    id="outputFilename"
    value="document_signed.pdf"&gt;
</code></pre>
<p>Make sure the filename has the correct extension:</p>
<pre><code class="language-javascript">function getOutputFilename() {

    let filename =
        outputFilename
            .value
            .trim();

    if (!filename) {

        filename =
            "document_signed.pdf";

    }

    if (
        !filename
            .toLowerCase()
            .endsWith(".pdf")
    ) {

        filename += ".pdf";

    }

    return filename;

}
</code></pre>
<p>The result section can also display the total page count and generated file size.</p>
<pre><code class="language-javascript">function formatFileSize(
    bytes
) {

    if (
        bytes &lt;
        1024 * 1024
    ) {

        return (
            bytes / 1024
        ).toFixed(2) + " KB";

    }

    return (
        bytes /
        1024 /
        1024
    ).toFixed(2) + " MB";

}
</code></pre>
<p>Update the file information:</p>
<pre><code class="language-javascript">filePageCount.textContent =
    `Total Pages: ${finalPdfDocument.numPages}`;

fileSize.textContent =
    `File Size: ${
        formatFileSize(
            finalPdfBlob.size
        )
    }`;
</code></pre>
<p>Download the file using a temporary object URL:</p>
<pre><code class="language-javascript">downloadButton.addEventListener(
    "click",
    () =&gt; {

        const url =
            URL.createObjectURL(
                finalPdfBlob
            );

        const link =
            document.createElement(
                "a"
            );

        link.href =
            url;

        link.download =
            getOutputFilename();

        link.click();

        URL.revokeObjectURL(
            url
        );

    }
);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/96212b85-3504-4420-bfba-819c20ca5a6e.png" alt="Signed PDF download section with editable filename, total page count, file size, and Download button." style="display:block;margin:0 auto" width="355" height="280" loading="lazy">

<p>After downloading, the <strong>Start Over</strong> button resets the application so another PDF can be processed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/fd08536f-4b47-4b7b-930c-5943685ad58e.png" alt="Start Over button for clearing the current PDF signing session and uploading another document." style="display:block;margin:0 auto" width="150" height="54" loading="lazy">

<h2 id="heading-demo-how-the-pdf-signature-tool-works">Demo: How the PDF Signature Tool Works</h2>
<p>Let's walk through the complete workflow from upload to download.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>.</p>
<p>The browser reads the document and prepares it for local processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/aa49b177-d750-4432-80db-51101eaa5650.png" alt="PDF Signature Tool upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="639" height="652" loading="lazy">

<h3 id="heading-step-2-preview-and-navigate-the-document">Step 2: Preview and Navigate the Document</h3>
<p>After upload, the current page appears in the PDF preview.</p>
<p>Previous and next controls allow users to navigate through multi-page documents and find the page where an element needs to be added.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/61827efd-9872-4499-8d61-96f7a7c6a821.png" alt="Uploaded PDF preview with previous and next page navigation controls." style="display:block;margin:0 auto" width="708" height="550" loading="lazy">

<h3 id="heading-step-3-choose-what-to-add">Step 3: Choose What to Add</h3>
<p>The user chooses between <strong>Signature</strong> and <strong>Text/Stamp</strong>.</p>
<p>This determines which creation controls appear in the editor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ba697ba0-2e86-4c35-9510-39503e706ac8.png" alt="PDF editing controls for choosing between a signature and a text or stamp element." style="display:block;margin:0 auto" width="342" height="755" loading="lazy">

<h3 id="heading-step-4-create-the-signature">Step 4: Create the Signature</h3>
<p>If Signature is selected, users can choose <strong>Draw</strong>, <strong>Type</strong>, or <strong>Upload</strong>.</p>
<p>Drawing works directly inside the signature canvas. The Type option creates a signature-style element from entered text, while Upload accepts an existing signature image.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/971b15a8-4322-45c3-83fb-cbb8eae3172d.png" alt="PDF signature creation controls with Draw, Type, and Upload options." style="display:block;margin:0 auto" width="543" height="350" loading="lazy">

<h3 id="heading-step-5-add-custom-text-or-a-preset-stamp">Step 5: Add Custom Text or a Preset Stamp</h3>
<p>Instead of a signature, users can select <strong>Text/Stamp</strong>.</p>
<p>They can enter custom content or choose a preset such as <strong>APPROVED</strong>, <strong>CONFIDENTIAL</strong>, <strong>DRAFT</strong>, or <strong>PAID</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/572bbac7-2b4d-49bd-8cae-399ca82c74b7.png" alt="Text and stamp controls with custom text and preset document stamp options." style="display:block;margin:0 auto" width="560" height="423" loading="lazy">

<h3 id="heading-step-6-position-and-style-the-element">Step 6: Position and Style the Element</h3>
<p>The created element appears over the PDF preview.</p>
<p>Users can drag it to the required position and adjust properties such as scale, rotation, opacity, X position, and Y position.</p>
<p>Text elements also support configurable font size and color.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d3424bda-ada1-4acd-8ce8-c55a631efc27.png" alt="Signature positioned over a PDF page with placement and styling controls." style="display:block;margin:0 auto" width="1047" height="532" loading="lazy">

<h3 id="heading-step-7-choose-the-target-pages">Step 7: Choose the Target Pages</h3>
<p>The element can be applied to the current page, every page, or a specific page selection.</p>
<p>For example:</p>
<pre><code class="language-text">1, 3-5, 10
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/10b0061f-57f2-4314-a7cb-41758bc00be6.png" alt="Choose the Target Pages or applied pages" style="display:block;margin:0 auto" width="329" height="252" loading="lazy">

<p>This is useful when the same stamp or document label needs to appear on several pages.</p>
<h3 id="heading-step-8-apply-and-finalize">Step 8: Apply and Finalize</h3>
<p>After checking the element and target pages, users click <strong>Apply &amp; Finalize</strong>.</p>
<p>The browser converts the preview position into PDF coordinates and generates the modified document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ad34b20-777d-49c9-8900-4876e22ac8c1.png" alt="Apply and Finalize button for generating the PDF with the selected signature or text element." style="display:block;margin:0 auto" width="204" height="73" loading="lazy">

<h3 id="heading-step-9-preview-the-completed-pdf">Step 9: Preview the Completed PDF</h3>
<p>The generated document appears in a final preview.</p>
<p>Users can navigate through the pages and verify that the signature, text, or stamp appears correctly before downloading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f7d699e6-bbb3-4692-b2bf-d2e1bb44bdc5.png" alt=" Completed PDF preview showing an applied signature before download." style="display:block;margin:0 auto" width="704" height="547" loading="lazy">

<h3 id="heading-step-10-rename-and-download">Step 10: Rename and Download</h3>
<p>The final section allows users to change the output filename and review the total number of pages and file size.</p>
<p>Clicking <strong>Download</strong> saves the generated PDF locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/58186c5d-7f10-4860-b2c3-87f5862bd025.png" alt="Final PDF download section with filename editing, page count, file size, and Download button." style="display:block;margin:0 auto" width="355" height="280" loading="lazy">

<p>Afterward, <strong>Start Over</strong> clears the session and returns to the upload interface.</p>
<h2 id="heading-handling-signature-transparency">Handling Signature Transparency</h2>
<p>Uploaded signatures often look best when the background is transparent.</p>
<p>A transparent PNG contains only the visible signature strokes, allowing the original PDF content to remain visible around the signature.</p>
<p>A JPEG image, by comparison, usually includes a solid background. If the image was scanned from white paper, placing it on a colored PDF area may create a visible white rectangle.</p>
<p>For uploaded signatures, transparent PNG files are therefore usually the better option.</p>
<p>The same principle applies to drawn and typed signatures. When converting a canvas to PNG, avoid filling the canvas with a background color unless that background is intentionally required.</p>
<pre><code class="language-javascript">const signatureImage =
    signatureCanvas.toDataURL(
        "image/png"
    );
</code></pre>
<p>The transparent canvas can then be embedded directly into the PDF.</p>
<h2 id="heading-important-notes-and-common-mistakes">Important Notes and Common Mistakes</h2>
<p>One common mistake is assuming that the browser preview and the actual PDF use identical coordinates.</p>
<p>Always calculate the relationship between the canvas dimensions and the target PDF page before placing the final element.</p>
<pre><code class="language-javascript">const scaleX =
    pdfWidth /
    pdfCanvas.width;

const scaleY =
    pdfHeight /
    pdfCanvas.height;
</code></pre>
<p>Another issue occurs when the same element is applied to pages with different dimensions. A position that looks correct on an A4 page may not appear in the same visual location on a landscape or differently sized page.</p>
<p>Uploaded signature images should also be validated before processing.</p>
<pre><code class="language-javascript">const allowedTypes = [
    "image/png",
    "image/jpeg"
];

if (
    !allowedTypes.includes(
        file.type
    )
) {

    alert(
        "Please upload a PNG or JPEG image."
    );

    return;

}
</code></pre>
<p>Very large image files should be resized before embedding to avoid unnecessarily increasing the final PDF size.</p>
<p>Users should also review the completed document before downloading it. Rotation, scaling, or coordinate conversion errors are much easier to identify in the final preview than after the file has already been shared.</p>
<p>Finally, remember that this project adds a <strong>visual electronic signature</strong> to a PDF. It does not create a certificate-based cryptographic digital signature or provide automatic identity verification.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Signature Tool using JavaScript.</p>
<p>You learned how to upload and preview PDF documents, navigate between pages, create signatures by drawing, typing, or uploading an image, add custom text and preset stamps, position elements directly over a PDF preview, adjust their appearance, choose target pages, and generate the completed document with PDF-lib.</p>
<p>You also learned how browser coordinates are converted into PDF coordinates and why signature transparency matters when embedding images into a document.</p>
<p>The final workflow allows users to preview the completed PDF, rename the output file, review its page count and size, and download it directly from the browser.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/sign-pdf/">PDF Signature Tool</a>.</p>
<p>The project can be extended further with multiple elements per page, reusable signature profiles, date fields, initials, custom fonts, signature removal before finalization, or certificate-based digital signing through a dedicated signing infrastructure.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
